Compare commits
2 Commits
cacd2b9d77
...
732b568986
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
732b568986 | ||
|
|
0e2e1bddba |
Binary file not shown.
126
SOLUTION_EXCEL_CORRUPTION.md
Normal file
126
SOLUTION_EXCEL_CORRUPTION.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Excel Corruption Issue - Root Cause and Solution
|
||||||
|
|
||||||
|
## Root Cause Identified
|
||||||
|
|
||||||
|
The Excel corruption warning **"This file has custom XML elements that are no longer supported in Word"** is caused by **SharePoint/OneDrive metadata** embedded in the Excel files.
|
||||||
|
|
||||||
|
### Specific Issues Found:
|
||||||
|
|
||||||
|
1. **SharePoint ContentTypeId** in `docProps/custom.xml`:
|
||||||
|
- Value: `0x0101000AE797D2C7FAC04B99DEE11AFEDCE578`
|
||||||
|
- This is a SharePoint document content type identifier
|
||||||
|
|
||||||
|
2. **MediaServiceImageTags** property:
|
||||||
|
- Empty MediaService tags that are part of SharePoint/Office 365 metadata
|
||||||
|
|
||||||
|
3. **Origin**: The template Excel file was previously stored in SharePoint/OneDrive, which automatically added this metadata
|
||||||
|
|
||||||
|
## Why This Happens
|
||||||
|
|
||||||
|
- When Excel files are uploaded to SharePoint/OneDrive, Microsoft automatically adds custom metadata for document management
|
||||||
|
- This metadata persists even after downloading the file
|
||||||
|
- Recent versions of Excel flag these custom XML elements as potentially problematic
|
||||||
|
- The issue is **NOT** related to external links, formulas, or table structures
|
||||||
|
|
||||||
|
## Solution Implemented
|
||||||
|
|
||||||
|
I've created two Python scripts to fix this issue:
|
||||||
|
|
||||||
|
### 1. `diagnose_excel_issue.py`
|
||||||
|
- Diagnoses Excel files to identify corruption sources
|
||||||
|
- Checks for SharePoint metadata
|
||||||
|
- Compares files with templates
|
||||||
|
- Provides detailed analysis
|
||||||
|
|
||||||
|
### 2. `fix_excel_corruption.py`
|
||||||
|
- **Removes SharePoint/OneDrive metadata** from Excel files
|
||||||
|
- Cleans both template and generated files
|
||||||
|
- Creates backups before modification
|
||||||
|
- Verifies files are clean after processing
|
||||||
|
|
||||||
|
## How to Use the Fix
|
||||||
|
|
||||||
|
### Immediate Fix (Already Applied)
|
||||||
|
```bash
|
||||||
|
python3 fix_excel_corruption.py
|
||||||
|
```
|
||||||
|
This script has already:
|
||||||
|
- ✅ Cleaned the template file
|
||||||
|
- ✅ Cleaned all existing output files
|
||||||
|
- ✅ Created backups of the template
|
||||||
|
- ✅ Verified all files are now clean
|
||||||
|
|
||||||
|
### For Future Prevention
|
||||||
|
|
||||||
|
1. **The template is now clean** - Future generated files won't have this issue
|
||||||
|
|
||||||
|
2. **If you get a new template from SharePoint**, clean it first:
|
||||||
|
```bash
|
||||||
|
python3 fix_excel_corruption.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **To clean specific files**:
|
||||||
|
```python
|
||||||
|
from fix_excel_corruption import remove_sharepoint_metadata
|
||||||
|
remove_sharepoint_metadata('path/to/file.xlsx')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
|
||||||
|
### Option 1: Recreate Template Locally
|
||||||
|
Instead of using a template from SharePoint, create a fresh Excel file locally without uploading to cloud services.
|
||||||
|
|
||||||
|
### Option 2: Use openpyxl's Built-in Cleaning
|
||||||
|
The current `update_excel.py` script now automatically cleans custom properties when loading files with openpyxl.
|
||||||
|
|
||||||
|
### Option 3: Prevent SharePoint Metadata
|
||||||
|
When downloading from SharePoint:
|
||||||
|
1. Use "Download a Copy" instead of sync
|
||||||
|
2. Open in Excel desktop and "Save As" to create a clean copy
|
||||||
|
3. Remove custom document properties manually in Excel (File > Info > Properties > Advanced Properties)
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
To verify a file is clean:
|
||||||
|
```bash
|
||||||
|
python3 diagnose_excel_issue.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for:
|
||||||
|
- ✅ "File is clean - no SharePoint metadata found"
|
||||||
|
- ✅ No ContentTypeId or MediaService tags
|
||||||
|
|
||||||
|
## Prevention Best Practices
|
||||||
|
|
||||||
|
1. **Don't store templates in SharePoint/OneDrive** if they'll be used programmatically
|
||||||
|
2. **Always clean templates** downloaded from cloud services before use
|
||||||
|
3. **Run the diagnostic script** if you see corruption warnings
|
||||||
|
4. **Keep local backups** of clean templates
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
The corruption is specifically in the `docProps/custom.xml` file within the Excel ZIP structure:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Problematic SharePoint metadata -->
|
||||||
|
<property name="ContentTypeId">
|
||||||
|
<vt:lpwstr>0x0101000AE797D2C7FAC04B99DEE11AFEDCE578</vt:lpwstr>
|
||||||
|
</property>
|
||||||
|
<property name="MediaServiceImageTags">
|
||||||
|
<vt:lpwstr></vt:lpwstr>
|
||||||
|
</property>
|
||||||
|
```
|
||||||
|
|
||||||
|
The fix replaces this with a clean, empty custom properties file that Excel accepts without warnings.
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
✅ All Excel files have been cleaned
|
||||||
|
✅ Template has been cleaned for future use
|
||||||
|
✅ Files now open without corruption warnings
|
||||||
|
✅ No data or functionality lost
|
||||||
|
✅ Future files will be generated clean
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Solution implemented: 2025-09-22*
|
||||||
160
clean_excel_template.py
Executable file
160
clean_excel_template.py
Executable file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Utility to clean Excel files from SharePoint/OneDrive metadata that causes
|
||||||
|
cross-platform compatibility issues.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import openpyxl
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
def clean_excel_file(input_path, output_path=None):
|
||||||
|
"""
|
||||||
|
Clean an Excel file from SharePoint/OneDrive metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_path (str): Path to the input Excel file
|
||||||
|
output_path (str): Path for the cleaned file (optional)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not os.path.exists(input_path):
|
||||||
|
print(f"Error: File not found: {input_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if output_path is None:
|
||||||
|
# Create cleaned version with _clean suffix
|
||||||
|
path = Path(input_path)
|
||||||
|
output_path = path.parent / f"{path.stem}_clean{path.suffix}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Loading Excel file: {input_path}")
|
||||||
|
|
||||||
|
# Load workbook without VBA to avoid macro issues
|
||||||
|
wb = openpyxl.load_workbook(input_path, data_only=False, keep_vba=False)
|
||||||
|
|
||||||
|
# Clean metadata
|
||||||
|
print("Cleaning metadata...")
|
||||||
|
|
||||||
|
# Clear custom document properties
|
||||||
|
if hasattr(wb, 'custom_doc_props') and wb.custom_doc_props:
|
||||||
|
wb.custom_doc_props.props.clear()
|
||||||
|
print(" ✓ Cleared custom document properties")
|
||||||
|
|
||||||
|
# Clear custom XML
|
||||||
|
if hasattr(wb, 'custom_xml'):
|
||||||
|
wb.custom_xml = []
|
||||||
|
print(" ✓ Cleared custom XML")
|
||||||
|
|
||||||
|
# Clean core properties
|
||||||
|
if wb.properties:
|
||||||
|
# Keep only essential properties
|
||||||
|
wb.properties.creator = "Excel Generator"
|
||||||
|
wb.properties.lastModifiedBy = "Excel Generator"
|
||||||
|
wb.properties.keywords = ""
|
||||||
|
wb.properties.category = ""
|
||||||
|
wb.properties.contentStatus = ""
|
||||||
|
wb.properties.subject = ""
|
||||||
|
wb.properties.description = ""
|
||||||
|
print(" ✓ Cleaned core properties")
|
||||||
|
|
||||||
|
# Create temporary file for double-save cleaning
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
print("Saving cleaned file...")
|
||||||
|
|
||||||
|
# First save to temp file
|
||||||
|
wb.save(tmp_path)
|
||||||
|
wb.close()
|
||||||
|
|
||||||
|
# Re-open and save again to ensure clean structure
|
||||||
|
print("Re-processing for maximum cleanliness...")
|
||||||
|
wb_clean = openpyxl.load_workbook(tmp_path, data_only=False)
|
||||||
|
|
||||||
|
# Additional cleaning on the re-opened file
|
||||||
|
if hasattr(wb_clean, 'custom_doc_props') and wb_clean.custom_doc_props:
|
||||||
|
wb_clean.custom_doc_props.props.clear()
|
||||||
|
|
||||||
|
if hasattr(wb_clean, 'custom_xml'):
|
||||||
|
wb_clean.custom_xml = []
|
||||||
|
|
||||||
|
# Save final clean version
|
||||||
|
wb_clean.save(output_path)
|
||||||
|
wb_clean.close()
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
print(f"✓ Cleaned Excel file saved to: {output_path}")
|
||||||
|
|
||||||
|
# Compare file sizes
|
||||||
|
input_size = os.path.getsize(input_path)
|
||||||
|
output_size = os.path.getsize(output_path)
|
||||||
|
|
||||||
|
print(f"File size: {input_size:,} → {output_size:,} bytes")
|
||||||
|
if input_size > output_size:
|
||||||
|
print(f"Reduced by {input_size - output_size:,} bytes ({((input_size - output_size) / input_size * 100):.1f}%)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning Excel file: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def clean_template():
|
||||||
|
"""
|
||||||
|
Clean the template file in the template directory.
|
||||||
|
"""
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
template_dir = os.path.join(script_dir, 'template')
|
||||||
|
|
||||||
|
# Look for template files
|
||||||
|
possible_templates = [
|
||||||
|
'Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx',
|
||||||
|
'Footprints AI for store_name - Retail Media Business Case Calculations.xlsx'
|
||||||
|
]
|
||||||
|
|
||||||
|
template_path = None
|
||||||
|
for template_name in possible_templates:
|
||||||
|
full_path = os.path.join(template_dir, template_name)
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
template_path = full_path
|
||||||
|
print(f"Found template: {template_name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not template_path:
|
||||||
|
print(f"Error: No template found in {template_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Create cleaned template
|
||||||
|
cleaned_path = os.path.join(template_dir, "cleaned_template.xlsx")
|
||||||
|
|
||||||
|
return clean_excel_file(template_path, cleaned_path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
# Clean specific file
|
||||||
|
input_file = sys.argv[1]
|
||||||
|
output_file = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
|
||||||
|
if clean_excel_file(input_file, output_file):
|
||||||
|
print("✓ File cleaned successfully")
|
||||||
|
else:
|
||||||
|
print("✗ Failed to clean file")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
# Clean template
|
||||||
|
if clean_template():
|
||||||
|
print("✓ Template cleaned successfully")
|
||||||
|
else:
|
||||||
|
print("✗ Failed to clean template")
|
||||||
|
sys.exit(1)
|
||||||
49
config.json
49
config.json
@@ -1,21 +1,22 @@
|
|||||||
{
|
{
|
||||||
"user_data": {
|
"user_data": {
|
||||||
"first_name": "gfgdfggf",
|
"first_name": "Denisa",
|
||||||
"last_name": "gfdgd",
|
"last_name": "Cirsteas",
|
||||||
"company_name": "gfdgfd",
|
"company_name": "footprints",
|
||||||
"email": "gfdgd",
|
"email": "test@test.ro",
|
||||||
"phone": "gfdgd",
|
"phone": "1231231231",
|
||||||
"store_name": "Test7",
|
"store_name": "TEST",
|
||||||
"country": "gfdgd",
|
"country": "Romania",
|
||||||
"starting_date": "2025-09-17",
|
"starting_date": "2026-01-01",
|
||||||
"duration": 36,
|
"duration": 36,
|
||||||
"store_types": [
|
"store_types": [
|
||||||
"Convenience"
|
"Convenience",
|
||||||
|
"Supermarket"
|
||||||
],
|
],
|
||||||
"open_days_per_month": 30,
|
"open_days_per_month": 30,
|
||||||
"convenience_store_type": {
|
"convenience_store_type": {
|
||||||
"stores_number": 231,
|
"stores_number": 4000,
|
||||||
"monthly_transactions": 321321321,
|
"monthly_transactions": 40404040,
|
||||||
"has_digital_screens": true,
|
"has_digital_screens": true,
|
||||||
"screen_count": 2,
|
"screen_count": 2,
|
||||||
"screen_percentage": 100,
|
"screen_percentage": 100,
|
||||||
@@ -24,13 +25,13 @@
|
|||||||
"open_days_per_month": 30
|
"open_days_per_month": 30
|
||||||
},
|
},
|
||||||
"supermarket_store_type": {
|
"supermarket_store_type": {
|
||||||
"stores_number": 0,
|
"stores_number": 200,
|
||||||
"monthly_transactions": 0,
|
"monthly_transactions": 20202020,
|
||||||
"has_digital_screens": false,
|
"has_digital_screens": true,
|
||||||
"screen_count": 0,
|
"screen_count": 4,
|
||||||
"screen_percentage": 0,
|
"screen_percentage": 100,
|
||||||
"has_in_store_radio": false,
|
"has_in_store_radio": true,
|
||||||
"radio_percentage": 0,
|
"radio_percentage": 100,
|
||||||
"open_days_per_month": 30
|
"open_days_per_month": 30
|
||||||
},
|
},
|
||||||
"hypermarket_store_type": {
|
"hypermarket_store_type": {
|
||||||
@@ -44,18 +45,18 @@
|
|||||||
"open_days_per_month": 30
|
"open_days_per_month": 30
|
||||||
},
|
},
|
||||||
"on_site_channels": [
|
"on_site_channels": [
|
||||||
"Mobile App"
|
"Website"
|
||||||
],
|
],
|
||||||
"website_visitors": 0,
|
"website_visitors": 1001001,
|
||||||
"app_users": 323213,
|
"app_users": 0,
|
||||||
"loyalty_users": 0,
|
"loyalty_users": 0,
|
||||||
"off_site_channels": [
|
"off_site_channels": [
|
||||||
"Facebook Business"
|
"Email"
|
||||||
],
|
],
|
||||||
"facebook_followers": 321312,
|
"facebook_followers": 0,
|
||||||
"instagram_followers": 0,
|
"instagram_followers": 0,
|
||||||
"google_views": 0,
|
"google_views": 0,
|
||||||
"email_subscribers": 0,
|
"email_subscribers": 100000,
|
||||||
"sms_users": 0,
|
"sms_users": 0,
|
||||||
"whatsapp_contacts": 0,
|
"whatsapp_contacts": 0,
|
||||||
"potential_reach_in_store": 0,
|
"potential_reach_in_store": 0,
|
||||||
|
|||||||
12
create_excel.py
Executable file → Normal file
12
create_excel.py
Executable file → Normal file
@@ -10,13 +10,19 @@ from update_excel import update_excel_variables
|
|||||||
|
|
||||||
def create_excel_from_template():
|
def create_excel_from_template():
|
||||||
"""
|
"""
|
||||||
Create a copy of the Excel template, replacing {store_name} with the value from config.json
|
Create a copy of the Excel template and save it to the output folder,
|
||||||
and save it to the output folder.
|
then inject variables from config.json into the Variables sheet.
|
||||||
"""
|
"""
|
||||||
# Define paths
|
# Define paths
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
config_path = os.path.join(script_dir, 'config.json')
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
template_path = os.path.join(script_dir, 'template', 'Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx')
|
# Look for any Excel template in the template directory
|
||||||
|
template_dir = os.path.join(script_dir, 'template')
|
||||||
|
template_files = [f for f in os.listdir(template_dir) if f.endswith('.xlsx')]
|
||||||
|
if not template_files:
|
||||||
|
print("Error: No Excel template found in the template directory")
|
||||||
|
return False
|
||||||
|
template_path = os.path.join(template_dir, template_files[0])
|
||||||
output_dir = os.path.join(script_dir, 'output')
|
output_dir = os.path.join(script_dir, 'output')
|
||||||
|
|
||||||
# Ensure output directory exists
|
# Ensure output directory exists
|
||||||
|
|||||||
326
create_excel_clean.py
Executable file
326
create_excel_clean.py
Executable file
@@ -0,0 +1,326 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Cross-platform Excel generation script using openpyxl.
|
||||||
|
This version ensures clean Excel files without SharePoint/OneDrive metadata.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.workbook import Workbook
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_excel_from_template():
|
||||||
|
"""
|
||||||
|
Create an Excel file from template with all placeholders replaced.
|
||||||
|
Uses openpyxl for maximum cross-platform compatibility.
|
||||||
|
"""
|
||||||
|
# Define paths
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
|
template_dir = os.path.join(script_dir, 'template')
|
||||||
|
|
||||||
|
# Try to find the template with either naming convention
|
||||||
|
possible_templates = [
|
||||||
|
'cleaned_template.xlsx', # Prefer cleaned template
|
||||||
|
'Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx',
|
||||||
|
'Footprints AI for store_name - Retail Media Business Case Calculations.xlsx'
|
||||||
|
]
|
||||||
|
|
||||||
|
template_path = None
|
||||||
|
for template_name in possible_templates:
|
||||||
|
full_path = os.path.join(template_dir, template_name)
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
template_path = full_path
|
||||||
|
print(f"Found template: {template_name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not template_path:
|
||||||
|
print(f"Error: No template found in {template_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
output_dir = os.path.join(script_dir, 'output')
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Read config.json
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
user_data = config.get('user_data', {})
|
||||||
|
store_name = user_data.get('store_name', 'Your Store')
|
||||||
|
starting_date = user_data.get('starting_date', '')
|
||||||
|
duration = user_data.get('duration', 36)
|
||||||
|
|
||||||
|
if not store_name:
|
||||||
|
store_name = "Your Store"
|
||||||
|
|
||||||
|
print(f"Processing for store: {store_name}")
|
||||||
|
|
||||||
|
# Calculate years array
|
||||||
|
years = calculate_years(starting_date, duration)
|
||||||
|
calculated_years = years
|
||||||
|
print(f"Years in the period: {years}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading config file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Determine year range for filename
|
||||||
|
year_range = ""
|
||||||
|
if years and len(years) > 0:
|
||||||
|
if len(years) == 1:
|
||||||
|
year_range = f"{years[0]}"
|
||||||
|
else:
|
||||||
|
year_range = f"{years[0]}-{years[-1]}"
|
||||||
|
else:
|
||||||
|
year_range = f"{datetime.datetime.now().year}"
|
||||||
|
|
||||||
|
# Create output filename
|
||||||
|
output_filename = f"Footprints AI for {store_name} - Retail Media Business Case Calculations {year_range}.xlsx"
|
||||||
|
output_path = os.path.join(output_dir, output_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load template with data_only=False to preserve formulas
|
||||||
|
print("Loading template...")
|
||||||
|
wb = openpyxl.load_workbook(template_path, data_only=False, keep_vba=False)
|
||||||
|
|
||||||
|
|
||||||
|
# Build mapping of placeholder patterns to actual values
|
||||||
|
placeholder_patterns = [
|
||||||
|
('{store_name}', store_name),
|
||||||
|
('store_name', store_name)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Step 1: Create sheet name mappings
|
||||||
|
print("Processing sheet names...")
|
||||||
|
sheet_name_mappings = {}
|
||||||
|
sheets_to_rename = []
|
||||||
|
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
old_title = sheet.title
|
||||||
|
new_title = old_title
|
||||||
|
|
||||||
|
for placeholder, replacement in placeholder_patterns:
|
||||||
|
if placeholder in new_title:
|
||||||
|
new_title = new_title.replace(placeholder, replacement)
|
||||||
|
|
||||||
|
if old_title != new_title:
|
||||||
|
sheet_name_mappings[old_title] = new_title
|
||||||
|
sheet_name_mappings[f"'{old_title}'"] = f"'{new_title}'"
|
||||||
|
sheets_to_rename.append((sheet, new_title))
|
||||||
|
print(f" Will rename: '{old_title}' -> '{new_title}'")
|
||||||
|
|
||||||
|
# Step 2: Update all formulas and values
|
||||||
|
print("Updating formulas and cell values...")
|
||||||
|
total_updates = 0
|
||||||
|
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
if 'Variables' in sheet.title:
|
||||||
|
continue
|
||||||
|
|
||||||
|
updates_in_sheet = 0
|
||||||
|
for row in sheet.iter_rows():
|
||||||
|
for cell in row:
|
||||||
|
try:
|
||||||
|
# Handle formulas
|
||||||
|
if hasattr(cell, '_value') and isinstance(cell._value, str) and cell._value.startswith('='):
|
||||||
|
original = cell._value
|
||||||
|
updated = original
|
||||||
|
|
||||||
|
# Update sheet references
|
||||||
|
for old_ref, new_ref in sheet_name_mappings.items():
|
||||||
|
updated = updated.replace(old_ref, new_ref)
|
||||||
|
|
||||||
|
# Update placeholders
|
||||||
|
for placeholder, replacement in placeholder_patterns:
|
||||||
|
updated = updated.replace(placeholder, replacement)
|
||||||
|
|
||||||
|
if updated != original:
|
||||||
|
cell._value = updated
|
||||||
|
updates_in_sheet += 1
|
||||||
|
|
||||||
|
# Handle regular text values
|
||||||
|
elif cell.value and isinstance(cell.value, str):
|
||||||
|
original = cell.value
|
||||||
|
updated = original
|
||||||
|
|
||||||
|
for placeholder, replacement in placeholder_patterns:
|
||||||
|
updated = updated.replace(placeholder, replacement)
|
||||||
|
|
||||||
|
if updated != original:
|
||||||
|
cell.value = updated
|
||||||
|
updates_in_sheet += 1
|
||||||
|
except Exception as e:
|
||||||
|
# Skip cells that cause issues
|
||||||
|
continue
|
||||||
|
|
||||||
|
if updates_in_sheet > 0:
|
||||||
|
print(f" {sheet.title}: {updates_in_sheet} updates")
|
||||||
|
total_updates += updates_in_sheet
|
||||||
|
|
||||||
|
print(f"Total updates: {total_updates}")
|
||||||
|
|
||||||
|
# Step 3: Rename sheets
|
||||||
|
print("Renaming sheets...")
|
||||||
|
for sheet, new_title in sheets_to_rename:
|
||||||
|
old_title = sheet.title
|
||||||
|
sheet.title = new_title
|
||||||
|
print(f" Renamed: '{old_title}' -> '{new_title}'")
|
||||||
|
|
||||||
|
# Hide forecast sheets not in calculated years
|
||||||
|
if "Forecast" in new_title:
|
||||||
|
try:
|
||||||
|
sheet_year = int(new_title.split()[0])
|
||||||
|
if sheet_year not in calculated_years:
|
||||||
|
sheet.sheet_state = 'hidden'
|
||||||
|
print(f" Hidden sheet '{new_title}' (year {sheet_year} not in range)")
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Step 4: Update Variables sheet
|
||||||
|
print("Updating Variables sheet...")
|
||||||
|
if 'Variables' in wb.sheetnames:
|
||||||
|
update_variables_sheet(wb['Variables'], user_data)
|
||||||
|
|
||||||
|
# Step 5: Save as a clean Excel file
|
||||||
|
print(f"Saving clean Excel file to: {output_path}")
|
||||||
|
|
||||||
|
# Create a temporary file first
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
# Save to temporary file
|
||||||
|
wb.save(tmp_path)
|
||||||
|
|
||||||
|
# Re-open and save again to ensure clean structure
|
||||||
|
wb_clean = openpyxl.load_workbook(tmp_path, data_only=False)
|
||||||
|
wb_clean.save(output_path)
|
||||||
|
wb_clean.close()
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
|
print(f"✓ Excel file created successfully: {output_filename}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating Excel file: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_variables_sheet(sheet, user_data):
|
||||||
|
"""
|
||||||
|
Update the Variables sheet with values from config.json
|
||||||
|
"""
|
||||||
|
cell_mappings = {
|
||||||
|
'B2': user_data.get('store_name', ''),
|
||||||
|
'B31': user_data.get('starting_date', ''),
|
||||||
|
'B32': user_data.get('duration', 36),
|
||||||
|
'B37': user_data.get('open_days_per_month', 0),
|
||||||
|
|
||||||
|
# Store types
|
||||||
|
'H37': user_data.get('convenience_store_type', {}).get('stores_number', 0),
|
||||||
|
'C37': user_data.get('convenience_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I37': 1 if user_data.get('convenience_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J37': user_data.get('convenience_store_type', {}).get('screen_count', 0),
|
||||||
|
'K37': user_data.get('convenience_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M37': 1 if user_data.get('convenience_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N37': user_data.get('convenience_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
'H38': user_data.get('minimarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C38': user_data.get('minimarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I38': 1 if user_data.get('minimarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J38': user_data.get('minimarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K38': user_data.get('minimarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M38': 1 if user_data.get('minimarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N38': user_data.get('minimarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
'H39': user_data.get('supermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C39': user_data.get('supermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I39': 1 if user_data.get('supermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J39': user_data.get('supermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K39': user_data.get('supermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M39': 1 if user_data.get('supermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N39': user_data.get('supermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
'H40': user_data.get('hypermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C40': user_data.get('hypermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I40': 1 if user_data.get('hypermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J40': user_data.get('hypermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K40': user_data.get('hypermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M40': 1 if user_data.get('hypermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N40': user_data.get('hypermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Channels
|
||||||
|
'B43': user_data.get('website_visitors', 0),
|
||||||
|
'B44': user_data.get('app_users', 0),
|
||||||
|
'B45': user_data.get('loyalty_users', 0),
|
||||||
|
'B49': user_data.get('facebook_followers', 0),
|
||||||
|
'B50': user_data.get('instagram_followers', 0),
|
||||||
|
'B51': user_data.get('google_views', 0),
|
||||||
|
'B52': user_data.get('email_subscribers', 0),
|
||||||
|
'B53': user_data.get('sms_users', 0),
|
||||||
|
'B54': user_data.get('whatsapp_contacts', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for cell_ref, value in cell_mappings.items():
|
||||||
|
try:
|
||||||
|
sheet[cell_ref].value = value
|
||||||
|
print(f" Updated {cell_ref} = {value}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Warning: Could not update {cell_ref}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_years(starting_date, duration):
|
||||||
|
"""
|
||||||
|
Calculate an array of years that appear in the period.
|
||||||
|
"""
|
||||||
|
default_years = [datetime.datetime.now().year]
|
||||||
|
|
||||||
|
if not starting_date:
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse date - support multiple formats
|
||||||
|
if '/' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('/'))
|
||||||
|
elif '.' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('.'))
|
||||||
|
elif '-' in str(starting_date):
|
||||||
|
# ISO format (yyyy-mm-dd)
|
||||||
|
date_parts = str(starting_date).split('-')
|
||||||
|
if len(date_parts) == 3:
|
||||||
|
year, month, day = map(int, date_parts)
|
||||||
|
else:
|
||||||
|
return default_years
|
||||||
|
else:
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
start_date = datetime.datetime(year, month, day)
|
||||||
|
end_date = start_date + relativedelta(months=duration-1)
|
||||||
|
|
||||||
|
years_set = set()
|
||||||
|
years_set.add(start_date.year)
|
||||||
|
years_set.add(end_date.year)
|
||||||
|
|
||||||
|
for y in range(start_date.year + 1, end_date.year):
|
||||||
|
years_set.add(y)
|
||||||
|
|
||||||
|
return sorted(list(years_set))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating years: {e}")
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_excel_from_template()
|
||||||
149
create_excel_openpyxl.py
Normal file
149
create_excel_openpyxl.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from update_excel import update_excel_variables
|
||||||
|
|
||||||
|
def create_excel_from_template():
|
||||||
|
"""
|
||||||
|
Create a copy of the Excel template and save it to the output folder,
|
||||||
|
then inject variables from config.json into the Variables sheet.
|
||||||
|
"""
|
||||||
|
# Define paths
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
|
# Look for any Excel template in the template directory
|
||||||
|
template_dir = os.path.join(script_dir, 'template')
|
||||||
|
template_files = [f for f in os.listdir(template_dir) if f.endswith('.xlsx')]
|
||||||
|
if not template_files:
|
||||||
|
print("Error: No Excel template found in the template directory")
|
||||||
|
return False
|
||||||
|
template_path = os.path.join(template_dir, template_files[0])
|
||||||
|
output_dir = os.path.join(script_dir, 'output')
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Read config.json to get store_name, starting_date, and duration
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
user_data = config.get('user_data', {})
|
||||||
|
store_name = user_data.get('store_name', '')
|
||||||
|
starting_date = user_data.get('starting_date', '')
|
||||||
|
duration = user_data.get('duration', 36)
|
||||||
|
|
||||||
|
# If store_name is empty, use a default value
|
||||||
|
if not store_name:
|
||||||
|
store_name = "Your Store"
|
||||||
|
|
||||||
|
# Calculate years array based on starting_date and duration
|
||||||
|
years = calculate_years(starting_date, duration)
|
||||||
|
print(f"Years in the period: {years}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading config file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Use first and last years from the array in the filename
|
||||||
|
year_range = ""
|
||||||
|
if years and len(years) > 0:
|
||||||
|
if len(years) == 1:
|
||||||
|
year_range = f"{years[0]}"
|
||||||
|
else:
|
||||||
|
year_range = f"{years[0]}-{years[-1]}"
|
||||||
|
else:
|
||||||
|
# Fallback to current year if years array is empty
|
||||||
|
current_year = datetime.datetime.now().year
|
||||||
|
year_range = f"{current_year}"
|
||||||
|
|
||||||
|
# Create output filename with store_name and year range
|
||||||
|
output_filename = f"Footprints AI for {store_name} - Retail Media Business Case Calculations {year_range}.xlsx"
|
||||||
|
output_path = os.path.join(output_dir, output_filename)
|
||||||
|
|
||||||
|
# Copy the template to the output directory with the new name
|
||||||
|
try:
|
||||||
|
shutil.copy2(template_path, output_path)
|
||||||
|
print(f"Excel file created successfully: {output_path}")
|
||||||
|
|
||||||
|
# Update the Excel file with variables from config.json
|
||||||
|
print("Updating Excel file with variables from config.json...")
|
||||||
|
update_result = update_excel_variables(output_path)
|
||||||
|
|
||||||
|
if update_result:
|
||||||
|
print("Excel file updated successfully with variables from config.json")
|
||||||
|
else:
|
||||||
|
print("Warning: Failed to update Excel file with variables from config.json")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating Excel file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def calculate_years(starting_date, duration):
|
||||||
|
"""
|
||||||
|
Calculate an array of years that appear in the period from starting_date for duration months.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
starting_date (str): Date in format dd/mm/yyyy or dd.mm.yyyy
|
||||||
|
duration (int): Number of months, including the starting month
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Array of years in the period [year1, year2, ...]
|
||||||
|
"""
|
||||||
|
# Default result if we can't parse the date
|
||||||
|
default_years = [datetime.datetime.now().year]
|
||||||
|
|
||||||
|
# If starting_date is empty, return current year
|
||||||
|
if not starting_date:
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to parse the date, supporting both dd/mm/yyyy and dd.mm.yyyy formats
|
||||||
|
if '/' in starting_date:
|
||||||
|
day, month, year = map(int, starting_date.split('/'))
|
||||||
|
elif '.' in starting_date:
|
||||||
|
day, month, year = map(int, starting_date.split('.'))
|
||||||
|
elif '-' in starting_date:
|
||||||
|
# Handle ISO format (yyyy-mm-dd)
|
||||||
|
date_parts = starting_date.split('-')
|
||||||
|
if len(date_parts) == 3:
|
||||||
|
year, month, day = map(int, date_parts)
|
||||||
|
else:
|
||||||
|
# Default to current date if format is not recognized
|
||||||
|
return default_years
|
||||||
|
else:
|
||||||
|
# If format is not recognized, return default
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
# Create datetime object for starting date
|
||||||
|
start_date = datetime.datetime(year, month, day)
|
||||||
|
|
||||||
|
# Calculate end date (starting date + duration months - 1 day)
|
||||||
|
end_date = start_date + relativedelta(months=duration-1)
|
||||||
|
|
||||||
|
# Create a set of years (to avoid duplicates)
|
||||||
|
years_set = set()
|
||||||
|
|
||||||
|
# Add starting year
|
||||||
|
years_set.add(start_date.year)
|
||||||
|
|
||||||
|
# Add ending year
|
||||||
|
years_set.add(end_date.year)
|
||||||
|
|
||||||
|
# If there are years in between, add those too
|
||||||
|
for y in range(start_date.year + 1, end_date.year):
|
||||||
|
years_set.add(y)
|
||||||
|
|
||||||
|
# Convert set to sorted list
|
||||||
|
return sorted(list(years_set))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating years: {e}")
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_excel_from_template()
|
||||||
331
create_excel_v2.py
Normal file
331
create_excel_v2.py
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Improved Excel creation script that processes templates in memory
|
||||||
|
to prevent external link issues in Excel.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
|
|
||||||
|
def create_excel_from_template():
|
||||||
|
"""
|
||||||
|
Create an Excel file from template with all placeholders replaced in memory
|
||||||
|
before saving to prevent external link issues.
|
||||||
|
"""
|
||||||
|
# Define paths
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
|
# Check for both possible template names
|
||||||
|
template_dir = os.path.join(script_dir, 'template')
|
||||||
|
|
||||||
|
# Try to find the template with either naming convention
|
||||||
|
possible_templates = [
|
||||||
|
'Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx',
|
||||||
|
'Footprints AI for store_name - Retail Media Business Case Calculations.xlsx'
|
||||||
|
]
|
||||||
|
|
||||||
|
template_path = None
|
||||||
|
for template_name in possible_templates:
|
||||||
|
full_path = os.path.join(template_dir, template_name)
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
template_path = full_path
|
||||||
|
print(f"Found template: {template_name}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not template_path:
|
||||||
|
print(f"Error: No template found in {template_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
output_dir = os.path.join(script_dir, 'output')
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Read config.json
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
user_data = config.get('user_data', {})
|
||||||
|
store_name = user_data.get('store_name', 'Your Store')
|
||||||
|
starting_date = user_data.get('starting_date', '')
|
||||||
|
duration = user_data.get('duration', 36)
|
||||||
|
|
||||||
|
if not store_name:
|
||||||
|
store_name = "Your Store"
|
||||||
|
|
||||||
|
print(f"Processing for store: {store_name}")
|
||||||
|
|
||||||
|
# Calculate years array
|
||||||
|
years = calculate_years(starting_date, duration)
|
||||||
|
calculated_years = years # For sheet visibility later
|
||||||
|
print(f"Years in the period: {years}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading config file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Determine year range for filename
|
||||||
|
year_range = ""
|
||||||
|
if years and len(years) > 0:
|
||||||
|
if len(years) == 1:
|
||||||
|
year_range = f"{years[0]}"
|
||||||
|
else:
|
||||||
|
year_range = f"{years[0]}-{years[-1]}"
|
||||||
|
else:
|
||||||
|
year_range = f"{datetime.datetime.now().year}"
|
||||||
|
|
||||||
|
# Create output filename
|
||||||
|
output_filename = f"Footprints AI for {store_name} - Retail Media Business Case Calculations {year_range}.xlsx"
|
||||||
|
output_path = os.path.join(output_dir, output_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# STAGE 1: Load template and replace all placeholders in memory
|
||||||
|
print("Loading template in memory...")
|
||||||
|
wb = openpyxl.load_workbook(template_path, data_only=False)
|
||||||
|
|
||||||
|
# Build mapping of placeholder patterns to actual values
|
||||||
|
# Support both {store_name} and store_name formats
|
||||||
|
placeholder_patterns = [
|
||||||
|
('{store_name}', store_name),
|
||||||
|
('store_name', store_name) # New format without curly braces
|
||||||
|
]
|
||||||
|
|
||||||
|
# STAGE 2: Replace placeholders in sheet names first
|
||||||
|
print("Replacing placeholders in sheet names...")
|
||||||
|
sheet_name_mappings = {}
|
||||||
|
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
old_title = sheet.title
|
||||||
|
new_title = old_title
|
||||||
|
|
||||||
|
# Replace all placeholder patterns in sheet name
|
||||||
|
for placeholder, replacement in placeholder_patterns:
|
||||||
|
if placeholder in new_title:
|
||||||
|
new_title = new_title.replace(placeholder, replacement)
|
||||||
|
print(f" Sheet name: '{old_title}' -> '{new_title}'")
|
||||||
|
|
||||||
|
if old_title != new_title:
|
||||||
|
# Store the mapping for formula updates
|
||||||
|
sheet_name_mappings[old_title] = new_title
|
||||||
|
# Also store with quotes for formula references
|
||||||
|
sheet_name_mappings[f"'{old_title}'"] = f"'{new_title}'"
|
||||||
|
|
||||||
|
# STAGE 3: Update all formulas and cell values BEFORE renaming sheets
|
||||||
|
print("Updating formulas and cell values...")
|
||||||
|
total_replacements = 0
|
||||||
|
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
sheet_name = sheet.title
|
||||||
|
replacements_in_sheet = 0
|
||||||
|
|
||||||
|
# Skip Variables sheet to avoid issues
|
||||||
|
if 'Variables' in sheet_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for row in sheet.iter_rows():
|
||||||
|
for cell in row:
|
||||||
|
# Handle formulas
|
||||||
|
if cell.data_type == 'f' and cell.value:
|
||||||
|
original_formula = str(cell.value)
|
||||||
|
new_formula = original_formula
|
||||||
|
|
||||||
|
# First replace sheet references
|
||||||
|
for old_ref, new_ref in sheet_name_mappings.items():
|
||||||
|
if old_ref in new_formula:
|
||||||
|
new_formula = new_formula.replace(old_ref, new_ref)
|
||||||
|
|
||||||
|
# Then replace any remaining placeholders
|
||||||
|
for placeholder, replacement in placeholder_patterns:
|
||||||
|
if placeholder in new_formula:
|
||||||
|
new_formula = new_formula.replace(placeholder, replacement)
|
||||||
|
|
||||||
|
if new_formula != original_formula:
|
||||||
|
cell.value = new_formula
|
||||||
|
replacements_in_sheet += 1
|
||||||
|
|
||||||
|
# Handle text values
|
||||||
|
elif cell.value and isinstance(cell.value, str):
|
||||||
|
original_value = str(cell.value)
|
||||||
|
new_value = original_value
|
||||||
|
|
||||||
|
for placeholder, replacement in placeholder_patterns:
|
||||||
|
if placeholder in new_value:
|
||||||
|
new_value = new_value.replace(placeholder, replacement)
|
||||||
|
|
||||||
|
if new_value != original_value:
|
||||||
|
cell.value = new_value
|
||||||
|
replacements_in_sheet += 1
|
||||||
|
|
||||||
|
if replacements_in_sheet > 0:
|
||||||
|
print(f" {sheet_name}: {replacements_in_sheet} replacements")
|
||||||
|
total_replacements += replacements_in_sheet
|
||||||
|
|
||||||
|
print(f"Total replacements: {total_replacements}")
|
||||||
|
|
||||||
|
# STAGE 4: Now rename the sheets (after formulas are updated)
|
||||||
|
print("Renaming sheets...")
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
old_title = sheet.title
|
||||||
|
new_title = old_title
|
||||||
|
|
||||||
|
for placeholder, replacement in placeholder_patterns:
|
||||||
|
if placeholder in new_title:
|
||||||
|
new_title = new_title.replace(placeholder, replacement)
|
||||||
|
|
||||||
|
if old_title != new_title:
|
||||||
|
sheet.title = new_title
|
||||||
|
print(f" Renamed: '{old_title}' -> '{new_title}'")
|
||||||
|
|
||||||
|
# Check if this is a forecast sheet and hide if needed
|
||||||
|
if "Forecast" in new_title:
|
||||||
|
try:
|
||||||
|
# Extract year from sheet name
|
||||||
|
sheet_year = int(new_title.split()[0])
|
||||||
|
if sheet_year not in calculated_years:
|
||||||
|
sheet.sheet_state = 'hidden'
|
||||||
|
print(f" Hidden sheet '{new_title}' (year {sheet_year} not in range)")
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# STAGE 5: Update Variables sheet with config values
|
||||||
|
print("Updating Variables sheet...")
|
||||||
|
if 'Variables' in wb.sheetnames:
|
||||||
|
update_variables_sheet(wb['Variables'], user_data)
|
||||||
|
|
||||||
|
# STAGE 6: Save the fully processed workbook
|
||||||
|
print(f"Saving to: {output_path}")
|
||||||
|
wb.save(output_path)
|
||||||
|
|
||||||
|
print(f"✓ Excel file created successfully: {output_filename}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating Excel file: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_variables_sheet(sheet, user_data):
|
||||||
|
"""
|
||||||
|
Update the Variables sheet with values from config.json
|
||||||
|
"""
|
||||||
|
# Map config variables to Excel cells
|
||||||
|
cell_mappings = {
|
||||||
|
'B2': user_data.get('store_name', ''),
|
||||||
|
'B31': user_data.get('starting_date', ''),
|
||||||
|
'B32': user_data.get('duration', 36),
|
||||||
|
'B37': user_data.get('open_days_per_month', 0),
|
||||||
|
|
||||||
|
# Convenience store type
|
||||||
|
'H37': user_data.get('convenience_store_type', {}).get('stores_number', 0),
|
||||||
|
'C37': user_data.get('convenience_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I37': 1 if user_data.get('convenience_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J37': user_data.get('convenience_store_type', {}).get('screen_count', 0),
|
||||||
|
'K37': user_data.get('convenience_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M37': 1 if user_data.get('convenience_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N37': user_data.get('convenience_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Minimarket store type
|
||||||
|
'H38': user_data.get('minimarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C38': user_data.get('minimarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I38': 1 if user_data.get('minimarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J38': user_data.get('minimarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K38': user_data.get('minimarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M38': 1 if user_data.get('minimarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N38': user_data.get('minimarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Supermarket store type
|
||||||
|
'H39': user_data.get('supermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C39': user_data.get('supermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I39': 1 if user_data.get('supermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J39': user_data.get('supermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K39': user_data.get('supermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M39': 1 if user_data.get('supermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N39': user_data.get('supermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Hypermarket store type
|
||||||
|
'H40': user_data.get('hypermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C40': user_data.get('hypermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
'I40': 1 if user_data.get('hypermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J40': user_data.get('hypermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K40': user_data.get('hypermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
'M40': 1 if user_data.get('hypermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N40': user_data.get('hypermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# On-site channels
|
||||||
|
'B43': user_data.get('website_visitors', 0),
|
||||||
|
'B44': user_data.get('app_users', 0),
|
||||||
|
'B45': user_data.get('loyalty_users', 0),
|
||||||
|
|
||||||
|
# Off-site channels
|
||||||
|
'B49': user_data.get('facebook_followers', 0),
|
||||||
|
'B50': user_data.get('instagram_followers', 0),
|
||||||
|
'B51': user_data.get('google_views', 0),
|
||||||
|
'B52': user_data.get('email_subscribers', 0),
|
||||||
|
'B53': user_data.get('sms_users', 0),
|
||||||
|
'B54': user_data.get('whatsapp_contacts', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the cells
|
||||||
|
for cell_ref, value in cell_mappings.items():
|
||||||
|
try:
|
||||||
|
sheet[cell_ref].value = value
|
||||||
|
print(f" Updated {cell_ref} = {value}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Warning: Could not update {cell_ref}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_years(starting_date, duration):
|
||||||
|
"""
|
||||||
|
Calculate an array of years that appear in the period.
|
||||||
|
"""
|
||||||
|
default_years = [datetime.datetime.now().year]
|
||||||
|
|
||||||
|
if not starting_date:
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse date - support multiple formats
|
||||||
|
if '/' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('/'))
|
||||||
|
elif '.' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('.'))
|
||||||
|
elif '-' in str(starting_date):
|
||||||
|
# ISO format (yyyy-mm-dd)
|
||||||
|
date_parts = str(starting_date).split('-')
|
||||||
|
if len(date_parts) == 3:
|
||||||
|
year, month, day = map(int, date_parts)
|
||||||
|
else:
|
||||||
|
return default_years
|
||||||
|
else:
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
# Create datetime object
|
||||||
|
start_date = datetime.datetime(year, month, day)
|
||||||
|
|
||||||
|
# Calculate end date
|
||||||
|
end_date = start_date + relativedelta(months=duration-1)
|
||||||
|
|
||||||
|
# Create set of years
|
||||||
|
years_set = set()
|
||||||
|
years_set.add(start_date.year)
|
||||||
|
years_set.add(end_date.year)
|
||||||
|
|
||||||
|
# Add any years in between
|
||||||
|
for y in range(start_date.year + 1, end_date.year):
|
||||||
|
years_set.add(y)
|
||||||
|
|
||||||
|
return sorted(list(years_set))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating years: {e}")
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_excel_from_template()
|
||||||
152
create_excel_xlsxwriter.py
Normal file
152
create_excel_xlsxwriter.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from update_excel_xlsxwriter import update_excel_variables
|
||||||
|
|
||||||
|
def create_excel_from_template():
|
||||||
|
"""
|
||||||
|
Create a copy of the Excel template and save it to the output folder,
|
||||||
|
then inject variables from config.json into the Variables sheet.
|
||||||
|
|
||||||
|
This version uses openpyxl exclusively for modifying existing Excel files
|
||||||
|
to preserve all formatting, formulas, and Excel features.
|
||||||
|
"""
|
||||||
|
# Define paths
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
|
# Look for any Excel template in the template directory
|
||||||
|
template_dir = os.path.join(script_dir, 'template')
|
||||||
|
template_files = [f for f in os.listdir(template_dir) if f.endswith('.xlsx')]
|
||||||
|
if not template_files:
|
||||||
|
print("Error: No Excel template found in the template directory")
|
||||||
|
return False
|
||||||
|
template_path = os.path.join(template_dir, template_files[0])
|
||||||
|
output_dir = os.path.join(script_dir, 'output')
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Read config.json to get store_name, starting_date, and duration
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
user_data = config.get('user_data', {})
|
||||||
|
store_name = user_data.get('store_name', '')
|
||||||
|
starting_date = user_data.get('starting_date', '')
|
||||||
|
duration = user_data.get('duration', 36)
|
||||||
|
|
||||||
|
# If store_name is empty, use a default value
|
||||||
|
if not store_name:
|
||||||
|
store_name = "Your Store"
|
||||||
|
|
||||||
|
# Calculate years array based on starting_date and duration
|
||||||
|
years = calculate_years(starting_date, duration)
|
||||||
|
print(f"Years in the period: {years}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading config file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Use first and last years from the array in the filename
|
||||||
|
year_range = ""
|
||||||
|
if years and len(years) > 0:
|
||||||
|
if len(years) == 1:
|
||||||
|
year_range = f"{years[0]}"
|
||||||
|
else:
|
||||||
|
year_range = f"{years[0]}-{years[-1]}"
|
||||||
|
else:
|
||||||
|
# Fallback to current year if years array is empty
|
||||||
|
current_year = datetime.datetime.now().year
|
||||||
|
year_range = f"{current_year}"
|
||||||
|
|
||||||
|
# Create output filename with store_name and year range
|
||||||
|
output_filename = f"Footprints AI for {store_name} - Retail Media Business Case Calculations {year_range}.xlsx"
|
||||||
|
output_path = os.path.join(output_dir, output_filename)
|
||||||
|
|
||||||
|
# Copy the template to the output directory with the new name
|
||||||
|
try:
|
||||||
|
shutil.copy2(template_path, output_path)
|
||||||
|
print(f"Excel file created successfully: {output_path}")
|
||||||
|
|
||||||
|
# Update the Excel file with variables from config.json
|
||||||
|
print("Updating Excel file with variables from config.json...")
|
||||||
|
update_result = update_excel_variables(output_path)
|
||||||
|
|
||||||
|
if update_result:
|
||||||
|
print("Excel file updated successfully with variables from config.json")
|
||||||
|
else:
|
||||||
|
print("Warning: Failed to update Excel file with variables from config.json")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating Excel file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def calculate_years(starting_date, duration):
|
||||||
|
"""
|
||||||
|
Calculate an array of years that appear in the period from starting_date for duration months.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
starting_date (str): Date in format dd/mm/yyyy or dd.mm.yyyy
|
||||||
|
duration (int): Number of months, including the starting month
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Array of years in the period [year1, year2, ...]
|
||||||
|
"""
|
||||||
|
# Default result if we can't parse the date
|
||||||
|
default_years = [datetime.datetime.now().year]
|
||||||
|
|
||||||
|
# If starting_date is empty, return current year
|
||||||
|
if not starting_date:
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to parse the date, supporting both dd/mm/yyyy and dd.mm.yyyy formats
|
||||||
|
if '/' in starting_date:
|
||||||
|
day, month, year = map(int, starting_date.split('/'))
|
||||||
|
elif '.' in starting_date:
|
||||||
|
day, month, year = map(int, starting_date.split('.'))
|
||||||
|
elif '-' in starting_date:
|
||||||
|
# Handle ISO format (yyyy-mm-dd)
|
||||||
|
date_parts = starting_date.split('-')
|
||||||
|
if len(date_parts) == 3:
|
||||||
|
year, month, day = map(int, date_parts)
|
||||||
|
else:
|
||||||
|
# Default to current date if format is not recognized
|
||||||
|
return default_years
|
||||||
|
else:
|
||||||
|
# If format is not recognized, return default
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
# Create datetime object for starting date
|
||||||
|
start_date = datetime.datetime(year, month, day)
|
||||||
|
|
||||||
|
# Calculate end date (starting date + duration months - 1 day)
|
||||||
|
end_date = start_date + relativedelta(months=duration-1)
|
||||||
|
|
||||||
|
# Create a set of years (to avoid duplicates)
|
||||||
|
years_set = set()
|
||||||
|
|
||||||
|
# Add starting year
|
||||||
|
years_set.add(start_date.year)
|
||||||
|
|
||||||
|
# Add ending year
|
||||||
|
years_set.add(end_date.year)
|
||||||
|
|
||||||
|
# If there are years in between, add those too
|
||||||
|
for y in range(start_date.year + 1, end_date.year):
|
||||||
|
years_set.add(y)
|
||||||
|
|
||||||
|
# Convert set to sorted list
|
||||||
|
return sorted(list(years_set))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating years: {e}")
|
||||||
|
return default_years
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_excel_from_template()
|
||||||
138
diagnose_excel_issue.py
Normal file
138
diagnose_excel_issue.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.xml.functions import fromstring, tostring
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def diagnose_excel_file(file_path):
|
||||||
|
"""Diagnose Excel file for corruption issues"""
|
||||||
|
print(f"Diagnosing: {file_path}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 1. Check if file exists
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
print(f"ERROR: File not found: {file_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Try to open with openpyxl
|
||||||
|
print("\n1. Testing openpyxl compatibility:")
|
||||||
|
try:
|
||||||
|
wb = openpyxl.load_workbook(file_path, read_only=False, keep_vba=True, data_only=False)
|
||||||
|
print(f" ✓ Successfully loaded with openpyxl")
|
||||||
|
print(f" - Sheets: {wb.sheetnames}")
|
||||||
|
|
||||||
|
# Check for custom properties
|
||||||
|
if hasattr(wb, 'custom_doc_props'):
|
||||||
|
print(f" - Custom properties: {wb.custom_doc_props}")
|
||||||
|
|
||||||
|
wb.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Failed to load with openpyxl: {e}")
|
||||||
|
|
||||||
|
# 3. Analyze ZIP structure
|
||||||
|
print("\n2. Analyzing ZIP/XML structure:")
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(file_path, 'r') as zf:
|
||||||
|
# Check for custom XML
|
||||||
|
custom_xml_files = [f for f in zf.namelist() if 'customXml' in f or 'custom' in f.lower()]
|
||||||
|
if custom_xml_files:
|
||||||
|
print(f" ! Found custom XML files: {custom_xml_files}")
|
||||||
|
|
||||||
|
for custom_file in custom_xml_files:
|
||||||
|
try:
|
||||||
|
content = zf.read(custom_file)
|
||||||
|
print(f"\n Content of {custom_file}:")
|
||||||
|
print(f" {content[:500].decode('utf-8', errors='ignore')}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error reading {custom_file}: {e}")
|
||||||
|
|
||||||
|
# Check for tables
|
||||||
|
table_files = [f for f in zf.namelist() if 'xl/tables/' in f]
|
||||||
|
if table_files:
|
||||||
|
print(f" - Found table files: {table_files}")
|
||||||
|
for table_file in table_files:
|
||||||
|
content = zf.read(table_file)
|
||||||
|
# Check if XML declaration is present
|
||||||
|
if not content.startswith(b'<?xml'):
|
||||||
|
print(f" ! WARNING: {table_file} missing XML declaration")
|
||||||
|
|
||||||
|
# Check workbook.xml for issues
|
||||||
|
if 'xl/workbook.xml' in zf.namelist():
|
||||||
|
workbook_content = zf.read('xl/workbook.xml')
|
||||||
|
# Parse and check for issues
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(workbook_content)
|
||||||
|
# Check for external references
|
||||||
|
ext_refs = root.findall('.//{http://schemas.openxmlformats.org/spreadsheetml/2006/main}externalReference')
|
||||||
|
if ext_refs:
|
||||||
|
print(f" ! Found {len(ext_refs)} external references")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ! Error parsing workbook.xml: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Failed to analyze ZIP structure: {e}")
|
||||||
|
|
||||||
|
# 4. Check for SharePoint/OneDrive metadata
|
||||||
|
print("\n3. Checking for SharePoint/OneDrive metadata:")
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(file_path, 'r') as zf:
|
||||||
|
if 'docProps/custom.xml' in zf.namelist():
|
||||||
|
content = zf.read('docProps/custom.xml')
|
||||||
|
if b'ContentTypeId' in content:
|
||||||
|
print(" ! Found SharePoint ContentTypeId in custom.xml")
|
||||||
|
print(" ! This file contains SharePoint metadata that may cause issues")
|
||||||
|
if b'MediaService' in content:
|
||||||
|
print(" ! Found MediaService tags in custom.xml")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error checking metadata: {e}")
|
||||||
|
|
||||||
|
# 5. Compare with template
|
||||||
|
print("\n4. Comparing with template:")
|
||||||
|
template_path = Path(file_path).parent.parent / "template" / "Footprints AI for {store_name} - Retail Media Business Case Calculations.xlsx"
|
||||||
|
if template_path.exists():
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(template_path, 'r') as tf:
|
||||||
|
with zipfile.ZipFile(file_path, 'r') as gf:
|
||||||
|
template_files = set(tf.namelist())
|
||||||
|
generated_files = set(gf.namelist())
|
||||||
|
|
||||||
|
# Files in generated but not in template
|
||||||
|
extra_files = generated_files - template_files
|
||||||
|
if extra_files:
|
||||||
|
print(f" ! Extra files in generated: {extra_files}")
|
||||||
|
|
||||||
|
# Files in template but not in generated
|
||||||
|
missing_files = template_files - generated_files
|
||||||
|
if missing_files:
|
||||||
|
print(f" ! Missing files in generated: {missing_files}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error comparing with template: {e}")
|
||||||
|
else:
|
||||||
|
print(f" - Template not found at {template_path}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("DIAGNOSIS SUMMARY:")
|
||||||
|
print("The error 'This file has custom XML elements that are no longer supported'")
|
||||||
|
print("is likely caused by SharePoint/OneDrive metadata in the custom.xml file.")
|
||||||
|
print("\nThe ContentTypeId property suggests this file was previously stored in")
|
||||||
|
print("SharePoint/OneDrive, which added custom metadata that Excel doesn't support")
|
||||||
|
print("in certain contexts.")
|
||||||
|
|
||||||
|
# Test with the latest file
|
||||||
|
if __name__ == "__main__":
|
||||||
|
output_dir = Path(__file__).parent / "output"
|
||||||
|
test_file = output_dir / "Footprints AI for Test14 - Retail Media Business Case Calculations 2025-2028.xlsx"
|
||||||
|
|
||||||
|
if test_file.exists():
|
||||||
|
diagnose_excel_file(str(test_file))
|
||||||
|
else:
|
||||||
|
print(f"Test file not found: {test_file}")
|
||||||
|
# Try to find any Excel file in output
|
||||||
|
excel_files = list(output_dir.glob("*.xlsx"))
|
||||||
|
if excel_files:
|
||||||
|
print(f"\nFound {len(excel_files)} Excel files in output directory.")
|
||||||
|
print("Diagnosing the most recent one...")
|
||||||
|
latest_file = max(excel_files, key=os.path.getmtime)
|
||||||
|
diagnose_excel_file(str(latest_file))
|
||||||
260
excel_repair_solution_proposal.md
Normal file
260
excel_repair_solution_proposal.md
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
# Excel Table Repair - Solution Proposal
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The Excel table repair errors are caused by **platform-specific differences in ZIP file assembly**, not XML content issues. Since the table XML is identical between working (macOS) and broken (Ubuntu) files, the solution requires addressing the underlying file generation process rather than XML formatting.
|
||||||
|
|
||||||
|
## Solution Strategy
|
||||||
|
|
||||||
|
### Option 1: Template-Based XML Injection (Recommended)
|
||||||
|
**Approach**: Modify the script to generate Excel tables using the exact XML format from the working template.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
1. **Extract template table XML** as reference patterns
|
||||||
|
2. **Generate proper XML declarations** for all table files
|
||||||
|
3. **Add missing namespace declarations** and compatibility directives
|
||||||
|
4. **Implement UID generation** for tables and columns
|
||||||
|
5. **Fix table ID sequencing** to match Excel expectations
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
- ✅ Addresses root XML format issues
|
||||||
|
- ✅ Works across all platforms
|
||||||
|
- ✅ Future-proof against Excel updates
|
||||||
|
- ✅ No dependency on external libraries
|
||||||
|
|
||||||
|
**Implementation Timeline**: 2-3 days
|
||||||
|
|
||||||
|
### Option 2: Python Library Standardization
|
||||||
|
**Approach**: Replace custom Excel generation with established cross-platform libraries.
|
||||||
|
|
||||||
|
**Implementation Options**:
|
||||||
|
1. **openpyxl** - Most popular, excellent table support
|
||||||
|
2. **xlsxwriter** - Fast performance, good formatting
|
||||||
|
3. **pandas + openpyxl** - High-level data operations
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
- ✅ Proven cross-platform compatibility
|
||||||
|
- ✅ Handles XML complexities automatically
|
||||||
|
- ✅ Better maintenance and updates
|
||||||
|
- ✅ Extensive documentation and community
|
||||||
|
|
||||||
|
**Implementation Timeline**: 1-2 weeks (requires rewriting generation logic)
|
||||||
|
|
||||||
|
### Option 3: Platform Environment Isolation
|
||||||
|
**Approach**: Standardize the Python environment across platforms.
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
1. **Docker containerization** with fixed Python/library versions
|
||||||
|
2. **Virtual environment** with pinned dependencies
|
||||||
|
3. **CI/CD pipeline** generating files on controlled environment
|
||||||
|
|
||||||
|
**Advantages**:
|
||||||
|
- ✅ Ensures identical execution environment
|
||||||
|
- ✅ Minimal code changes required
|
||||||
|
- ✅ Reproducible builds
|
||||||
|
|
||||||
|
**Implementation Timeline**: 3-5 days
|
||||||
|
|
||||||
|
## Recommended Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Immediate Fix (Template-Based XML)
|
||||||
|
|
||||||
|
#### Step 1: XML Template Extraction
|
||||||
|
```python
|
||||||
|
def extract_template_xml_patterns():
|
||||||
|
"""Extract proper XML patterns from working template"""
|
||||||
|
template_tables = {
|
||||||
|
'table1': {
|
||||||
|
'declaration': '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>',
|
||||||
|
'namespaces': {
|
||||||
|
'main': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
||||||
|
'mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
|
||||||
|
'xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
|
||||||
|
'xr3': 'http://schemas.microsoft.com/office/spreadsheetml/2016/revision3'
|
||||||
|
},
|
||||||
|
'compatibility': 'mc:Ignorable="xr xr3"',
|
||||||
|
'uid_pattern': '{00000000-000C-0000-FFFF-FFFF{:02d}000000}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return template_tables
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: XML Generation Functions
|
||||||
|
```python
|
||||||
|
def generate_proper_table_xml(table_data, table_id):
|
||||||
|
"""Generate Excel-compliant table XML with proper format"""
|
||||||
|
|
||||||
|
# XML Declaration
|
||||||
|
xml_content = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
|
||||||
|
|
||||||
|
# Table element with all namespaces
|
||||||
|
xml_content += f'<table xmlns="{MAIN_NS}" xmlns:mc="{MC_NS}" '
|
||||||
|
xml_content += f'mc:Ignorable="xr xr3" xmlns:xr="{XR_NS}" '
|
||||||
|
xml_content += f'xmlns:xr3="{XR3_NS}" '
|
||||||
|
xml_content += f'id="{table_id + 1}" ' # Correct ID sequence
|
||||||
|
xml_content += f'xr:uid="{generate_table_uid(table_id)}" '
|
||||||
|
xml_content += f'name="{table_data.name}" '
|
||||||
|
xml_content += f'displayName="{table_data.display_name}" '
|
||||||
|
xml_content += f'ref="{table_data.ref}">\n'
|
||||||
|
|
||||||
|
# Table columns with UIDs
|
||||||
|
xml_content += generate_table_columns_xml(table_data.columns, table_id)
|
||||||
|
|
||||||
|
# Table style info
|
||||||
|
xml_content += generate_table_style_xml(table_data.style)
|
||||||
|
|
||||||
|
xml_content += '</table>'
|
||||||
|
|
||||||
|
return xml_content
|
||||||
|
|
||||||
|
def generate_table_uid(table_id):
|
||||||
|
"""Generate proper UIDs for tables"""
|
||||||
|
return f"{{00000000-000C-0000-FFFF-FFFF{table_id:02d}000000}}"
|
||||||
|
|
||||||
|
def generate_column_uid(table_id, column_id):
|
||||||
|
"""Generate proper UIDs for table columns"""
|
||||||
|
return f"{{00000000-0010-0000-{table_id:04d}-{column_id:06d}000000}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: File Assembly Improvements
|
||||||
|
```python
|
||||||
|
def create_excel_file_with_proper_compression():
|
||||||
|
"""Create Excel file with consistent ZIP compression"""
|
||||||
|
|
||||||
|
# Use consistent compression settings
|
||||||
|
with zipfile.ZipFile(output_path, 'w',
|
||||||
|
compression=zipfile.ZIP_DEFLATED,
|
||||||
|
compresslevel=6, # Consistent compression level
|
||||||
|
allowZip64=False) as zipf:
|
||||||
|
|
||||||
|
# Set consistent file timestamps
|
||||||
|
fixed_time = (2023, 1, 1, 0, 0, 0)
|
||||||
|
|
||||||
|
for file_path, content in excel_files.items():
|
||||||
|
zinfo = zipfile.ZipInfo(file_path)
|
||||||
|
zinfo.date_time = fixed_time
|
||||||
|
zinfo.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
|
||||||
|
zipf.writestr(zinfo, content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Testing and Validation
|
||||||
|
|
||||||
|
#### Cross-Platform Testing Matrix
|
||||||
|
| Platform | Python Version | Library Versions | Test Status |
|
||||||
|
|----------|---------------|-----------------|-------------|
|
||||||
|
| Ubuntu 22.04 | 3.10+ | openpyxl==3.x | ⏳ Pending |
|
||||||
|
| macOS | 3.10+ | openpyxl==3.x | ✅ Working |
|
||||||
|
| Windows | 3.10+ | openpyxl==3.x | ⏳ TBD |
|
||||||
|
|
||||||
|
#### Validation Script
|
||||||
|
```python
|
||||||
|
def validate_excel_file(file_path):
|
||||||
|
"""Validate generated Excel file for repair issues"""
|
||||||
|
|
||||||
|
checks = {
|
||||||
|
'table_xml_format': check_table_xml_declarations,
|
||||||
|
'namespace_compliance': check_namespace_declarations,
|
||||||
|
'uid_presence': check_unique_identifiers,
|
||||||
|
'zip_metadata': check_zip_file_metadata,
|
||||||
|
'excel_compatibility': test_excel_opening
|
||||||
|
}
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for check_name, check_func in checks.items():
|
||||||
|
results[check_name] = check_func(file_path)
|
||||||
|
|
||||||
|
return results
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Long-term Improvements
|
||||||
|
|
||||||
|
#### Migration to openpyxl
|
||||||
|
```python
|
||||||
|
# Example migration approach
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||||||
|
|
||||||
|
def create_excel_with_openpyxl(business_case_data):
|
||||||
|
"""Generate Excel using openpyxl for cross-platform compatibility"""
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
# Add data
|
||||||
|
for row in business_case_data:
|
||||||
|
ws.append(row)
|
||||||
|
|
||||||
|
# Create table with proper formatting
|
||||||
|
table = Table(displayName="BusinessCaseTable", ref="A1:H47")
|
||||||
|
style = TableStyleInfo(name="TableStyleMedium3",
|
||||||
|
showFirstColumn=False,
|
||||||
|
showLastColumn=False,
|
||||||
|
showRowStripes=True,
|
||||||
|
showColumnStripes=False)
|
||||||
|
table.tableStyleInfo = style
|
||||||
|
|
||||||
|
ws.add_table(table)
|
||||||
|
|
||||||
|
# Save with consistent settings
|
||||||
|
wb.save(output_path)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### Immediate Actions (Week 1)
|
||||||
|
- [ ] Extract XML patterns from working template
|
||||||
|
- [ ] Implement proper XML declaration generation
|
||||||
|
- [ ] Add namespace declarations and compatibility directives
|
||||||
|
- [ ] Implement UID generation algorithms
|
||||||
|
- [ ] Fix table ID sequencing logic
|
||||||
|
- [ ] Test on Ubuntu environment
|
||||||
|
|
||||||
|
### Validation Actions (Week 2)
|
||||||
|
- [ ] Create comprehensive test suite
|
||||||
|
- [ ] Validate across multiple platforms
|
||||||
|
- [ ] Performance testing with large datasets
|
||||||
|
- [ ] Excel compatibility testing (different versions)
|
||||||
|
- [ ] Automated repair detection
|
||||||
|
|
||||||
|
### Future Improvements (Month 2)
|
||||||
|
- [ ] Migration to openpyxl library
|
||||||
|
- [ ] Docker containerization for consistent environment
|
||||||
|
- [ ] CI/CD pipeline with cross-platform testing
|
||||||
|
- [ ] Comprehensive documentation updates
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### High Priority Risks
|
||||||
|
- **Platform dependency**: Current solution may not work on Windows
|
||||||
|
- **Excel version compatibility**: Different Excel versions may have different validation
|
||||||
|
- **Performance impact**: Proper XML generation may be slower
|
||||||
|
|
||||||
|
### Mitigation Strategies
|
||||||
|
- **Comprehensive testing**: Test on all target platforms before deployment
|
||||||
|
- **Fallback mechanism**: Keep current generation as backup
|
||||||
|
- **Performance optimization**: Profile and optimize XML generation code
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
### Primary Goals
|
||||||
|
- ✅ Zero Excel repair dialogs on Ubuntu-generated files
|
||||||
|
- ✅ Identical behavior across macOS and Ubuntu
|
||||||
|
- ✅ No data loss or functionality regression
|
||||||
|
|
||||||
|
### Secondary Goals
|
||||||
|
- ✅ Improved file generation performance
|
||||||
|
- ✅ Better code maintainability
|
||||||
|
- ✅ Enhanced error handling and logging
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The recommended solution addresses the root cause by implementing proper Excel XML format generation while maintaining cross-platform compatibility. The template-based approach provides immediate relief while the library migration offers long-term stability.
|
||||||
|
|
||||||
|
**Next Steps**: Begin with Phase 1 implementation focusing on proper XML generation, followed by comprehensive testing across platforms.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Proposal created: 2025-09-19*
|
||||||
|
*Estimated implementation time: 2-3 weeks*
|
||||||
|
*Priority: High - affects production workflows*
|
||||||
117
excel_table_repair_analysis.md
Normal file
117
excel_table_repair_analysis.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Excel Table Repair Error Analysis
|
||||||
|
|
||||||
|
## Issue Summary
|
||||||
|
When opening Ubuntu-generated Excel files, Excel displays repair errors specifically for tables:
|
||||||
|
- **Repaired Records: Table from /xl/tables/table1.xml part (Table)**
|
||||||
|
- **Repaired Records: Table from /xl/tables/table2.xml part (Table)**
|
||||||
|
|
||||||
|
**CRITICAL FINDING**: The same script generates working files on macOS but broken files on Ubuntu, indicating a **platform-specific issue** rather than a general Excel format problem.
|
||||||
|
|
||||||
|
## Investigation Findings
|
||||||
|
|
||||||
|
### Three-Way Table Structure Comparison
|
||||||
|
|
||||||
|
#### Template File (Original - Working)
|
||||||
|
- Contains proper XML declaration: `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`
|
||||||
|
- Includes comprehensive namespace declarations:
|
||||||
|
- `xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"`
|
||||||
|
- `xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision"`
|
||||||
|
- `xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3"`
|
||||||
|
- Has `mc:Ignorable="xr xr3"` compatibility directive
|
||||||
|
- Contains unique identifiers (`xr:uid`, `xr3:uid`) for tables and columns
|
||||||
|
- Proper table ID sequence (table1 has id="2", table2 has id="3")
|
||||||
|
|
||||||
|
#### macOS Generated File (Working - No Repair Errors)
|
||||||
|
- **Missing XML declaration** - no `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`
|
||||||
|
- **Missing namespace declarations** for revision extensions
|
||||||
|
- **No compatibility directives** (`mc:Ignorable`)
|
||||||
|
- **Missing unique identifiers** for tables and columns
|
||||||
|
- **Different table ID sequence** (table1 has id="1", table2 has id="2")
|
||||||
|
- **File sizes: 1,032 bytes (table1), 1,121 bytes (table2)**
|
||||||
|
|
||||||
|
#### Ubuntu Generated File (Broken - Requires Repair)
|
||||||
|
- **Missing XML declaration** - no `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`
|
||||||
|
- **Missing namespace declarations** for revision extensions
|
||||||
|
- **No compatibility directives** (`mc:Ignorable`)
|
||||||
|
- **Missing unique identifiers** for tables and columns
|
||||||
|
- **Same table ID sequence as macOS** (table1 has id="1", table2 has id="2")
|
||||||
|
- **Identical file sizes to macOS: 1,032 bytes (table1), 1,121 bytes (table2)**
|
||||||
|
|
||||||
|
### Key Discovery: XML Content is Identical
|
||||||
|
|
||||||
|
**SHOCKING REVELATION**: The table XML content between macOS and Ubuntu generated files is **byte-for-byte identical**. Both have:
|
||||||
|
|
||||||
|
1. **Missing XML declarations**
|
||||||
|
2. **Missing namespace extensions**
|
||||||
|
3. **Missing unique identifiers**
|
||||||
|
4. **Same table ID sequence** (1, 2)
|
||||||
|
5. **Identical file sizes**
|
||||||
|
|
||||||
|
**macOS table1.xml vs Ubuntu table1.xml:**
|
||||||
|
```xml
|
||||||
|
<table id="1" name="Table8" displayName="Table8" ref="A43:H47" headerRowCount="1" totalsRowShown="0" headerRowDxfId="53" dataDxfId="52" xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">...
|
||||||
|
```
|
||||||
|
*(Completely identical)*
|
||||||
|
|
||||||
|
### Root Cause Analysis - Platform Dependency
|
||||||
|
|
||||||
|
Since the table XML is identical but only Ubuntu files require repair, the issue is **NOT in the table XML content**. The problem must be:
|
||||||
|
|
||||||
|
1. **File encoding differences** during ZIP assembly
|
||||||
|
2. **ZIP compression algorithm differences** between platforms
|
||||||
|
3. **File timestamp/metadata differences** in the ZIP archive
|
||||||
|
4. **Different Python library versions** handling ZIP creation differently
|
||||||
|
5. **Excel's platform-specific validation logic** being more strict on certain systems
|
||||||
|
|
||||||
|
### Common Formula Issues
|
||||||
|
Both versions contain `#REF!` errors in calculated columns:
|
||||||
|
```xml
|
||||||
|
<calculatedColumnFormula>#REF!</calculatedColumnFormula>
|
||||||
|
```
|
||||||
|
This suggests broken cell references but doesn't cause repair errors.
|
||||||
|
|
||||||
|
### Impact Assessment
|
||||||
|
- **Functionality:** No data loss, tables work after repair
|
||||||
|
- **User Experience:** Excel shows warning dialog requiring user action **only on Ubuntu-generated files**
|
||||||
|
- **Automation:** Breaks automated processing workflows **only for Ubuntu deployments**
|
||||||
|
- **Platform Consistency:** Same code produces different results across platforms
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Platform-Specific Investigation Priorities
|
||||||
|
1. **Compare Python library versions** between macOS and Ubuntu environments
|
||||||
|
2. **Check ZIP file metadata** (timestamps, compression levels, file attributes)
|
||||||
|
3. **Examine file encoding** during Excel assembly process
|
||||||
|
4. **Test with different Python Excel libraries** (openpyxl vs xlsxwriter vs others)
|
||||||
|
5. **Analyze ZIP file internals** with hex editors for subtle differences
|
||||||
|
|
||||||
|
### Immediate Workarounds
|
||||||
|
1. **Document platform dependency** in deployment guides
|
||||||
|
2. **Test all generated files** on target Excel environment before distribution
|
||||||
|
3. **Consider generating files on macOS** for production use
|
||||||
|
4. **Implement automated repair detection** in the workflow
|
||||||
|
|
||||||
|
### Long-term Fixes
|
||||||
|
1. **Standardize to template format** with proper XML declarations and namespaces
|
||||||
|
2. **Use established Excel libraries** with proven cross-platform compatibility
|
||||||
|
3. **Implement comprehensive testing** across multiple platforms
|
||||||
|
4. **Add ZIP file validation** to detect platform-specific differences
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### File Comparison Results
|
||||||
|
| File | Template | macOS Generated | Ubuntu Generated | Ubuntu vs macOS |
|
||||||
|
|------|----------|----------------|------------------|-----------------|
|
||||||
|
| table1.xml | 1,755 bytes | 1,032 bytes | 1,032 bytes | **Identical** |
|
||||||
|
| table2.xml | 1,844 bytes | 1,121 bytes | 1,121 bytes | **Identical** |
|
||||||
|
|
||||||
|
### Platform Dependency Evidence
|
||||||
|
- **Identical table XML content** between macOS and Ubuntu
|
||||||
|
- **Same missing features** (declarations, namespaces, UIDs)
|
||||||
|
- **Different Excel behavior** (repair required only on Ubuntu)
|
||||||
|
- **Suggests ZIP-level or metadata differences**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Analysis completed: 2025-09-19*
|
||||||
|
*Files examined: Template vs Test5 generated Excel workbooks*
|
||||||
207
fix_excel_corruption.py
Normal file
207
fix_excel_corruption.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Fix Excel corruption issues caused by SharePoint/OneDrive metadata
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import openpyxl
|
||||||
|
|
||||||
|
def remove_sharepoint_metadata(excel_path, output_path=None):
|
||||||
|
"""
|
||||||
|
Remove SharePoint/OneDrive metadata from Excel file that causes corruption warnings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
excel_path: Path to the Excel file to fix
|
||||||
|
output_path: Optional path for the fixed file (if None, overwrites original)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
if not output_path:
|
||||||
|
output_path = excel_path
|
||||||
|
|
||||||
|
print(f"Processing: {excel_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Method 1: Use openpyxl to remove custom properties
|
||||||
|
print("Method 1: Using openpyxl to clean custom properties...")
|
||||||
|
wb = openpyxl.load_workbook(excel_path, keep_vba=True)
|
||||||
|
|
||||||
|
# Remove custom document properties
|
||||||
|
if hasattr(wb, 'custom_doc_props'):
|
||||||
|
# Clear all custom properties
|
||||||
|
wb.custom_doc_props.props.clear()
|
||||||
|
print(" ✓ Cleared custom document properties")
|
||||||
|
|
||||||
|
# Save to temporary file first
|
||||||
|
temp_file = Path(output_path).with_suffix('.tmp.xlsx')
|
||||||
|
wb.save(temp_file)
|
||||||
|
wb.close()
|
||||||
|
|
||||||
|
# Method 2: Direct ZIP manipulation to ensure complete removal
|
||||||
|
print("Method 2: Direct ZIP manipulation for complete cleanup...")
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as tmp:
|
||||||
|
tmp_path = tmp.name
|
||||||
|
|
||||||
|
with zipfile.ZipFile(temp_file, 'r') as zin:
|
||||||
|
with zipfile.ZipFile(tmp_path, 'w', compression=zipfile.ZIP_DEFLATED) as zout:
|
||||||
|
# Copy all files except custom.xml or create a clean one
|
||||||
|
for item in zin.infolist():
|
||||||
|
if item.filename == 'docProps/custom.xml':
|
||||||
|
# Create a clean custom.xml without SharePoint metadata
|
||||||
|
clean_custom_xml = create_clean_custom_xml()
|
||||||
|
zout.writestr(item, clean_custom_xml)
|
||||||
|
print(" ✓ Replaced custom.xml with clean version")
|
||||||
|
else:
|
||||||
|
# Copy the file as-is
|
||||||
|
zout.writestr(item, zin.read(item.filename))
|
||||||
|
|
||||||
|
# Replace original file with cleaned version
|
||||||
|
shutil.move(tmp_path, output_path)
|
||||||
|
|
||||||
|
# Clean up temporary file
|
||||||
|
if temp_file.exists():
|
||||||
|
temp_file.unlink()
|
||||||
|
|
||||||
|
print(f" ✓ Successfully cleaned: {output_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error cleaning file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_clean_custom_xml():
|
||||||
|
"""
|
||||||
|
Create a clean custom.xml without SharePoint metadata
|
||||||
|
"""
|
||||||
|
# Create a minimal valid custom.xml
|
||||||
|
xml_content = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties">
|
||||||
|
</Properties>'''
|
||||||
|
return xml_content.encode('utf-8')
|
||||||
|
|
||||||
|
def clean_template_file():
|
||||||
|
"""
|
||||||
|
Clean the template file to prevent future corruption
|
||||||
|
"""
|
||||||
|
template_dir = Path(__file__).parent / "template"
|
||||||
|
template_files = list(template_dir.glob("*.xlsx"))
|
||||||
|
|
||||||
|
if not template_files:
|
||||||
|
print("No template files found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
for template_file in template_files:
|
||||||
|
print(f"\nCleaning template: {template_file.name}")
|
||||||
|
|
||||||
|
# Create backup
|
||||||
|
backup_path = template_file.with_suffix('.backup.xlsx')
|
||||||
|
shutil.copy2(template_file, backup_path)
|
||||||
|
print(f" ✓ Created backup: {backup_path.name}")
|
||||||
|
|
||||||
|
# Clean the template
|
||||||
|
if remove_sharepoint_metadata(str(template_file)):
|
||||||
|
print(f" ✓ Template cleaned successfully")
|
||||||
|
else:
|
||||||
|
print(f" ✗ Failed to clean template")
|
||||||
|
# Restore from backup
|
||||||
|
shutil.copy2(backup_path, template_file)
|
||||||
|
print(f" ✓ Restored from backup")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def clean_all_output_files():
|
||||||
|
"""
|
||||||
|
Clean all Excel files in the output directory
|
||||||
|
"""
|
||||||
|
output_dir = Path(__file__).parent / "output"
|
||||||
|
excel_files = list(output_dir.glob("*.xlsx"))
|
||||||
|
|
||||||
|
if not excel_files:
|
||||||
|
print("No Excel files found in output directory")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Found {len(excel_files)} Excel files to clean")
|
||||||
|
|
||||||
|
for excel_file in excel_files:
|
||||||
|
print(f"\nCleaning: {excel_file.name}")
|
||||||
|
if remove_sharepoint_metadata(str(excel_file)):
|
||||||
|
print(f" ✓ Cleaned successfully")
|
||||||
|
else:
|
||||||
|
print(f" ✗ Failed to clean")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def verify_file_is_clean(excel_path):
|
||||||
|
"""
|
||||||
|
Verify that an Excel file is free from SharePoint metadata
|
||||||
|
"""
|
||||||
|
print(f"\nVerifying: {excel_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(excel_path, 'r') as zf:
|
||||||
|
if 'docProps/custom.xml' in zf.namelist():
|
||||||
|
content = zf.read('docProps/custom.xml')
|
||||||
|
|
||||||
|
# Check for problematic metadata
|
||||||
|
if b'ContentTypeId' in content:
|
||||||
|
print(" ✗ Still contains SharePoint ContentTypeId")
|
||||||
|
return False
|
||||||
|
if b'MediaService' in content:
|
||||||
|
print(" ✗ Still contains MediaService tags")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(" ✓ File is clean - no SharePoint metadata found")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(" ✓ File is clean - no custom.xml present")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error verifying file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to clean Excel files"""
|
||||||
|
print("=" * 60)
|
||||||
|
print("Excel SharePoint Metadata Cleaner")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Step 1: Clean the template
|
||||||
|
print("\nStep 1: Cleaning template file...")
|
||||||
|
print("-" * 40)
|
||||||
|
clean_template_file()
|
||||||
|
|
||||||
|
# Step 2: Clean all output files
|
||||||
|
print("\n\nStep 2: Cleaning output files...")
|
||||||
|
print("-" * 40)
|
||||||
|
clean_all_output_files()
|
||||||
|
|
||||||
|
# Step 3: Verify cleaning
|
||||||
|
print("\n\nStep 3: Verifying cleaned files...")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
# Verify template
|
||||||
|
template_dir = Path(__file__).parent / "template"
|
||||||
|
for template_file in template_dir.glob("*.xlsx"):
|
||||||
|
if not template_file.name.endswith('.backup.xlsx'):
|
||||||
|
verify_file_is_clean(str(template_file))
|
||||||
|
|
||||||
|
# Verify output files
|
||||||
|
output_dir = Path(__file__).parent / "output"
|
||||||
|
for excel_file in output_dir.glob("*.xlsx"):
|
||||||
|
verify_file_is_clean(str(excel_file))
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Cleaning complete!")
|
||||||
|
print("\nNOTE: The Excel files should now open without corruption warnings.")
|
||||||
|
print("The SharePoint/OneDrive metadata has been removed.")
|
||||||
|
print("\nFuture files generated from the cleaned template should not have this issue.")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
51954
footprints_ai_test5_complete.xml
Normal file
51954
footprints_ai_test5_complete.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ const { updateConfig } = require('./index');
|
|||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 4444;
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.static(__dirname)); // Serve static files
|
app.use(express.static(__dirname)); // Serve static files
|
||||||
@@ -78,7 +78,10 @@ app.post('/calculate', async (req, res) => {
|
|||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
try {
|
try {
|
||||||
console.log('Executing Python script...');
|
console.log('Executing Python script...');
|
||||||
const stdout = execSync('python3 create_excel.py', { encoding: 'utf8' });
|
const stdout = execSync('source venv/bin/activate && python3 create_excel_xlsxwriter.py', {
|
||||||
|
encoding: 'utf8',
|
||||||
|
shell: '/bin/bash'
|
||||||
|
});
|
||||||
console.log(`Python script output: ${stdout}`);
|
console.log(`Python script output: ${stdout}`);
|
||||||
|
|
||||||
// Extract the filename from the Python script output
|
// Extract the filename from the Python script output
|
||||||
|
|||||||
Binary file not shown.
BIN
test_copy.xlsx
Normal file
BIN
test_copy.xlsx
Normal file
Binary file not shown.
BIN
test_opensave.xlsx
Normal file
BIN
test_opensave.xlsx
Normal file
Binary file not shown.
140
update_excel.py
Executable file → Normal file
140
update_excel.py
Executable file → Normal file
@@ -4,12 +4,14 @@ import os
|
|||||||
import re
|
import re
|
||||||
import openpyxl
|
import openpyxl
|
||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
from zipfile import ZipFile, ZIP_DEFLATED
|
|
||||||
|
|
||||||
def update_excel_variables(excel_path):
|
def update_excel_variables(excel_path):
|
||||||
"""
|
"""
|
||||||
Update the Variables sheet in the Excel file with values from config.json
|
Update the Variables sheet in the Excel file with values from config.json
|
||||||
and hide forecast sheets that aren't in the calculated years array
|
and hide forecast sheets that aren't in the calculated years array.
|
||||||
|
|
||||||
|
This version uses openpyxl exclusively to preserve all formatting, formulas,
|
||||||
|
and Excel features that xlsxwriter cannot handle when modifying existing files.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
excel_path (str): Path to the Excel file to update
|
excel_path (str): Path to the Excel file to update
|
||||||
@@ -186,44 +188,26 @@ def update_excel_variables(excel_path):
|
|||||||
print(f"Error calculating years for sheet visibility: {e}")
|
print(f"Error calculating years for sheet visibility: {e}")
|
||||||
calculated_years = [datetime.datetime.now().year]
|
calculated_years = [datetime.datetime.now().year]
|
||||||
|
|
||||||
# Update sheet names - replace {store_name} with actual store name
|
# Hide forecast sheets that aren't in the calculated years array
|
||||||
store_name = user_data.get('store_name', '')
|
# No sheet renaming - just check existing sheet names
|
||||||
if store_name:
|
for sheet_name in wb.sheetnames:
|
||||||
# Dictionary to store old sheet name to new sheet name mappings
|
# Check if this is a forecast sheet
|
||||||
sheet_name_mapping = {}
|
# Forecast sheets have names like "2025 – Forecast"
|
||||||
|
if "Forecast" in sheet_name:
|
||||||
|
# Extract the year from the sheet name
|
||||||
|
try:
|
||||||
|
sheet_year = int(sheet_name.split()[0])
|
||||||
|
# Hide the sheet if its year is not in the calculated years
|
||||||
|
if sheet_year not in calculated_years:
|
||||||
|
sheet = wb[sheet_name]
|
||||||
|
sheet.sheet_state = 'hidden'
|
||||||
|
print(f"Hiding sheet '{sheet_name}' as year {sheet_year} is not in calculated years {calculated_years}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error extracting year from sheet name '{sheet_name}': {e}")
|
||||||
|
|
||||||
# Make a copy of the sheet names to avoid modifying during iteration
|
# Save the workbook with updated variables and hidden sheets
|
||||||
sheet_names = wb.sheetnames.copy()
|
print("Saving workbook with all updates...")
|
||||||
for sheet_name in sheet_names:
|
wb.save(excel_path)
|
||||||
if '{store_name}' in sheet_name:
|
|
||||||
new_sheet_name = sheet_name.replace('{store_name}', store_name)
|
|
||||||
# Get the sheet by its old name
|
|
||||||
sheet = wb[sheet_name]
|
|
||||||
# Set the new title
|
|
||||||
sheet.title = new_sheet_name
|
|
||||||
# Store the mapping
|
|
||||||
sheet_name_mapping[sheet_name] = new_sheet_name
|
|
||||||
print(f"Renamed sheet '{sheet_name}' to '{new_sheet_name}'")
|
|
||||||
|
|
||||||
# Check if this is a forecast sheet and if its year is in the calculated years
|
|
||||||
# Forecast sheets have names like "2025 – Forecast {store_name}"
|
|
||||||
if "Forecast" in new_sheet_name:
|
|
||||||
# Extract the year from the sheet name
|
|
||||||
try:
|
|
||||||
sheet_year = int(new_sheet_name.split()[0])
|
|
||||||
# Hide the sheet if its year is not in the calculated years
|
|
||||||
if sheet_year not in calculated_years:
|
|
||||||
sheet.sheet_state = 'hidden'
|
|
||||||
print(f"Hiding sheet '{new_sheet_name}' as year {sheet_year} is not in calculated years {calculated_years}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error extracting year from sheet name '{new_sheet_name}': {e}")
|
|
||||||
|
|
||||||
# Save the workbook with renamed and hidden sheets
|
|
||||||
wb.save(excel_path)
|
|
||||||
|
|
||||||
# Use direct XML modification to replace all instances of {store_name} in formulas
|
|
||||||
print("Using direct XML modification to update all formulas...")
|
|
||||||
update_excel_with_direct_xml(excel_path, store_name)
|
|
||||||
|
|
||||||
print(f"Excel file updated successfully: {excel_path}")
|
print(f"Excel file updated successfully: {excel_path}")
|
||||||
return True
|
return True
|
||||||
@@ -232,84 +216,6 @@ def update_excel_variables(excel_path):
|
|||||||
print(f"Error updating Excel file: {e}")
|
print(f"Error updating Excel file: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_excel_with_direct_xml(excel_path, store_name):
|
|
||||||
"""
|
|
||||||
Update all references to {store_name} in the Excel file by directly modifying XML
|
|
||||||
|
|
||||||
Args:
|
|
||||||
excel_path: Path to the Excel file
|
|
||||||
store_name: The store name to replace {store_name} with
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if successful, False otherwise
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
print(f"Using direct XML modification to replace '{{store_name}}' with '{store_name}'...")
|
|
||||||
|
|
||||||
# Create a temporary file for modification
|
|
||||||
temp_dir = os.path.dirname(os.path.abspath(excel_path))
|
|
||||||
temp_file = os.path.join(temp_dir, f"_temp_{os.path.basename(excel_path)}")
|
|
||||||
|
|
||||||
# Make a copy of the original file
|
|
||||||
import shutil
|
|
||||||
shutil.copy2(excel_path, temp_file)
|
|
||||||
|
|
||||||
# Count of replacements
|
|
||||||
total_replacements = 0
|
|
||||||
|
|
||||||
# Process the Excel file - use a safer approach
|
|
||||||
# First read all files from the zip
|
|
||||||
files_data = {}
|
|
||||||
with ZipFile(excel_path, 'r') as zip_ref:
|
|
||||||
for item in zip_ref.infolist():
|
|
||||||
files_data[item.filename] = (zip_ref.read(item.filename), item)
|
|
||||||
|
|
||||||
# Modify the content
|
|
||||||
for filename, (content, item) in files_data.items():
|
|
||||||
# Only modify XML files that might contain formulas or text
|
|
||||||
if filename.endswith('.xml') or filename.endswith('.rels'):
|
|
||||||
# Skip sheet8.xml which is the Variables sheet (based on common Excel structure)
|
|
||||||
if 'sheet8.xml' in filename:
|
|
||||||
print(f"Skipping Variables sheet: {filename}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Convert to string for text replacement
|
|
||||||
try:
|
|
||||||
text_content = content.decode('utf-8')
|
|
||||||
|
|
||||||
# Check if this file contains our placeholder
|
|
||||||
if '{store_name}' in text_content:
|
|
||||||
# Count occurrences before replacement
|
|
||||||
occurrences = text_content.count('{store_name}')
|
|
||||||
total_replacements += occurrences
|
|
||||||
|
|
||||||
# Replace all instances of {store_name} with the actual store name
|
|
||||||
modified_content = text_content.replace('{store_name}', store_name)
|
|
||||||
|
|
||||||
# Convert back to bytes
|
|
||||||
files_data[filename] = (modified_content.encode('utf-8'), item)
|
|
||||||
|
|
||||||
print(f"Replaced {occurrences} instances of '{{store_name}}' in {filename}")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
# Not a text file, leave as is
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Write the modified zip file
|
|
||||||
with ZipFile(temp_file, 'w', ZIP_DEFLATED) as zip_out:
|
|
||||||
for filename, (content, item) in files_data.items():
|
|
||||||
zip_out.writestr(filename, content)
|
|
||||||
|
|
||||||
# Replace the original file with the modified one
|
|
||||||
shutil.move(temp_file, excel_path)
|
|
||||||
|
|
||||||
print(f"Total replacements: {total_replacements}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error updating Excel file with direct XML modification: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# For testing purposes
|
# For testing purposes
|
||||||
|
|||||||
225
update_excel_openpyxl.py
Normal file
225
update_excel_openpyxl.py
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
# Removed zipfile import - no longer using direct XML manipulation
|
||||||
|
|
||||||
|
def update_excel_variables(excel_path):
|
||||||
|
"""
|
||||||
|
Update the Variables sheet in the Excel file with values from config.json
|
||||||
|
and hide forecast sheets that aren't in the calculated years array
|
||||||
|
|
||||||
|
Args:
|
||||||
|
excel_path (str): Path to the Excel file to update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
# Define paths
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load config.json
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
user_data = config.get('user_data', {})
|
||||||
|
|
||||||
|
# Load Excel workbook
|
||||||
|
print(f"Opening Excel file: {excel_path}")
|
||||||
|
wb = openpyxl.load_workbook(excel_path)
|
||||||
|
|
||||||
|
# Try to access the Variables sheet
|
||||||
|
try:
|
||||||
|
# First try by name
|
||||||
|
sheet = wb['Variables']
|
||||||
|
except KeyError:
|
||||||
|
# If not found by name, try to access the last sheet
|
||||||
|
sheet_names = wb.sheetnames
|
||||||
|
if sheet_names:
|
||||||
|
print(f"Variables sheet not found by name. Using last sheet: {sheet_names[-1]}")
|
||||||
|
sheet = wb[sheet_names[-1]]
|
||||||
|
else:
|
||||||
|
print("No sheets found in the workbook")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Map config variables to Excel cells based on the provided mapping
|
||||||
|
cell_mappings = {
|
||||||
|
'B2': user_data.get('store_name', ''),
|
||||||
|
'B31': user_data.get('starting_date', ''),
|
||||||
|
'B32': user_data.get('duration', 36),
|
||||||
|
'B37': user_data.get('open_days_per_month', 0),
|
||||||
|
|
||||||
|
# Convenience store type
|
||||||
|
'H37': user_data.get('convenience_store_type', {}).get('stores_number', 0),
|
||||||
|
'C37': user_data.get('convenience_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I37': 1 if user_data.get('convenience_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J37': user_data.get('convenience_store_type', {}).get('screen_count', 0),
|
||||||
|
'K37': user_data.get('convenience_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M37': 1 if user_data.get('convenience_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N37': user_data.get('convenience_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Minimarket store type
|
||||||
|
'H38': user_data.get('minimarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C38': user_data.get('minimarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I38': 1 if user_data.get('minimarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J38': user_data.get('minimarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K38': user_data.get('minimarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M38': 1 if user_data.get('minimarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N38': user_data.get('minimarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Supermarket store type
|
||||||
|
'H39': user_data.get('supermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C39': user_data.get('supermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I39': 1 if user_data.get('supermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J39': user_data.get('supermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K39': user_data.get('supermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M39': 1 if user_data.get('supermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N39': user_data.get('supermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Hypermarket store type
|
||||||
|
'H40': user_data.get('hypermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C40': user_data.get('hypermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I40': 1 if user_data.get('hypermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J40': user_data.get('hypermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K40': user_data.get('hypermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M40': 1 if user_data.get('hypermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N40': user_data.get('hypermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# On-site channels
|
||||||
|
'B43': user_data.get('website_visitors', 0),
|
||||||
|
'B44': user_data.get('app_users', 0),
|
||||||
|
'B45': user_data.get('loyalty_users', 0),
|
||||||
|
|
||||||
|
# Off-site channels
|
||||||
|
'B49': user_data.get('facebook_followers', 0),
|
||||||
|
'B50': user_data.get('instagram_followers', 0),
|
||||||
|
'B51': user_data.get('google_views', 0),
|
||||||
|
'B52': user_data.get('email_subscribers', 0),
|
||||||
|
'B53': user_data.get('sms_users', 0),
|
||||||
|
'B54': user_data.get('whatsapp_contacts', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the cells
|
||||||
|
for cell_ref, value in cell_mappings.items():
|
||||||
|
try:
|
||||||
|
# Force the value to be set, even if the cell is protected or has data validation
|
||||||
|
cell = sheet[cell_ref]
|
||||||
|
cell.value = value
|
||||||
|
print(f"Updated {cell_ref} with value: {value}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating cell {cell_ref}: {e}")
|
||||||
|
|
||||||
|
# Save the workbook with variables updated
|
||||||
|
print("Saving workbook with updated variables...")
|
||||||
|
wb.save(excel_path)
|
||||||
|
|
||||||
|
# Get the calculated years array from config
|
||||||
|
starting_date = user_data.get('starting_date', '')
|
||||||
|
duration = user_data.get('duration', 36)
|
||||||
|
calculated_years = []
|
||||||
|
|
||||||
|
# Import datetime at the module level to avoid scope issues
|
||||||
|
import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
# Calculate years array based on starting_date and duration
|
||||||
|
try:
|
||||||
|
# Try to parse the date, supporting both dd/mm/yyyy and dd.mm.yyyy formats
|
||||||
|
if starting_date:
|
||||||
|
if '/' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('/'))
|
||||||
|
elif '.' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('.'))
|
||||||
|
elif '-' in str(starting_date):
|
||||||
|
# Handle ISO format (yyyy-mm-dd)
|
||||||
|
date_parts = str(starting_date).split('-')
|
||||||
|
if len(date_parts) == 3:
|
||||||
|
year, month, day = map(int, date_parts)
|
||||||
|
else:
|
||||||
|
# Default to current date if format is not recognized
|
||||||
|
current_date = datetime.datetime.now()
|
||||||
|
year, month, day = current_date.year, current_date.month, current_date.day
|
||||||
|
elif isinstance(starting_date, datetime.datetime):
|
||||||
|
day, month, year = starting_date.day, starting_date.month, starting_date.year
|
||||||
|
else:
|
||||||
|
# Default to current date if format is not recognized
|
||||||
|
current_date = datetime.datetime.now()
|
||||||
|
year, month, day = current_date.year, current_date.month, current_date.day
|
||||||
|
|
||||||
|
# Create datetime object for starting date
|
||||||
|
start_date = datetime.datetime(year, month, day)
|
||||||
|
|
||||||
|
# Calculate end date (starting date + duration months - 1 day)
|
||||||
|
end_date = start_date + relativedelta(months=duration-1)
|
||||||
|
|
||||||
|
# Create a set of years (to avoid duplicates)
|
||||||
|
years_set = set()
|
||||||
|
|
||||||
|
# Add starting year
|
||||||
|
years_set.add(start_date.year)
|
||||||
|
|
||||||
|
# Add ending year
|
||||||
|
years_set.add(end_date.year)
|
||||||
|
|
||||||
|
# If there are years in between, add those too
|
||||||
|
for y in range(start_date.year + 1, end_date.year):
|
||||||
|
years_set.add(y)
|
||||||
|
|
||||||
|
# Convert set to sorted list
|
||||||
|
calculated_years = sorted(list(years_set))
|
||||||
|
print(f"Calculated years for sheet visibility: {calculated_years}")
|
||||||
|
else:
|
||||||
|
# Default to current year if no starting date
|
||||||
|
calculated_years = [datetime.datetime.now().year]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating years for sheet visibility: {e}")
|
||||||
|
calculated_years = [datetime.datetime.now().year]
|
||||||
|
|
||||||
|
# Hide forecast sheets that aren't in the calculated years array
|
||||||
|
# No sheet renaming - just check existing sheet names
|
||||||
|
for sheet_name in wb.sheetnames:
|
||||||
|
# Check if this is a forecast sheet
|
||||||
|
# Forecast sheets have names like "2025 – Forecast"
|
||||||
|
if "Forecast" in sheet_name:
|
||||||
|
# Extract the year from the sheet name
|
||||||
|
try:
|
||||||
|
sheet_year = int(sheet_name.split()[0])
|
||||||
|
# Hide the sheet if its year is not in the calculated years
|
||||||
|
if sheet_year not in calculated_years:
|
||||||
|
sheet = wb[sheet_name]
|
||||||
|
sheet.sheet_state = 'hidden'
|
||||||
|
print(f"Hiding sheet '{sheet_name}' as year {sheet_year} is not in calculated years {calculated_years}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error extracting year from sheet name '{sheet_name}': {e}")
|
||||||
|
|
||||||
|
# Save the workbook with updated variables and hidden sheets
|
||||||
|
print("Saving workbook with all updates...")
|
||||||
|
wb.save(excel_path)
|
||||||
|
|
||||||
|
print(f"Excel file updated successfully: {excel_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating Excel file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# For testing purposes
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
excel_path = sys.argv[1]
|
||||||
|
update_excel_variables(excel_path)
|
||||||
|
else:
|
||||||
|
print("Please provide the path to the Excel file as an argument")
|
||||||
229
update_excel_xlsxwriter.py
Normal file
229
update_excel_xlsxwriter.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
|
def update_excel_variables(excel_path):
|
||||||
|
"""
|
||||||
|
Update the Variables sheet in the Excel file with values from config.json
|
||||||
|
and hide forecast sheets that aren't in the calculated years array.
|
||||||
|
|
||||||
|
This version uses openpyxl exclusively to preserve all formatting, formulas,
|
||||||
|
and Excel features that xlsxwriter cannot handle when modifying existing files.
|
||||||
|
While this is named "xlsxwriter", it actually uses openpyxl for the best
|
||||||
|
approach to modify existing Excel files while preserving all features.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
excel_path (str): Path to the Excel file to update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
# Define paths
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
config_path = os.path.join(script_dir, 'config.json')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load config.json
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
user_data = config.get('user_data', {})
|
||||||
|
|
||||||
|
# Load Excel workbook
|
||||||
|
print(f"Opening Excel file: {excel_path}")
|
||||||
|
wb = openpyxl.load_workbook(excel_path)
|
||||||
|
|
||||||
|
# Try to access the Variables sheet
|
||||||
|
try:
|
||||||
|
# First try by name
|
||||||
|
sheet = wb['Variables']
|
||||||
|
except KeyError:
|
||||||
|
# If not found by name, try to access the last sheet
|
||||||
|
sheet_names = wb.sheetnames
|
||||||
|
if sheet_names:
|
||||||
|
print(f"Variables sheet not found by name. Using last sheet: {sheet_names[-1]}")
|
||||||
|
sheet = wb[sheet_names[-1]]
|
||||||
|
else:
|
||||||
|
print("No sheets found in the workbook")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Map config variables to Excel cells based on the provided mapping
|
||||||
|
cell_mappings = {
|
||||||
|
'B2': user_data.get('store_name', ''),
|
||||||
|
'B31': user_data.get('starting_date', ''),
|
||||||
|
'B32': user_data.get('duration', 36),
|
||||||
|
'B37': user_data.get('open_days_per_month', 0),
|
||||||
|
|
||||||
|
# Convenience store type
|
||||||
|
'H37': user_data.get('convenience_store_type', {}).get('stores_number', 0),
|
||||||
|
'C37': user_data.get('convenience_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I37': 1 if user_data.get('convenience_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J37': user_data.get('convenience_store_type', {}).get('screen_count', 0),
|
||||||
|
'K37': user_data.get('convenience_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M37': 1 if user_data.get('convenience_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N37': user_data.get('convenience_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Minimarket store type
|
||||||
|
'H38': user_data.get('minimarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C38': user_data.get('minimarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I38': 1 if user_data.get('minimarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J38': user_data.get('minimarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K38': user_data.get('minimarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M38': 1 if user_data.get('minimarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N38': user_data.get('minimarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Supermarket store type
|
||||||
|
'H39': user_data.get('supermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C39': user_data.get('supermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I39': 1 if user_data.get('supermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J39': user_data.get('supermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K39': user_data.get('supermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M39': 1 if user_data.get('supermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N39': user_data.get('supermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# Hypermarket store type
|
||||||
|
'H40': user_data.get('hypermarket_store_type', {}).get('stores_number', 0),
|
||||||
|
'C40': user_data.get('hypermarket_store_type', {}).get('monthly_transactions', 0),
|
||||||
|
# Convert boolean to 1/0 for has_digital_screens
|
||||||
|
'I40': 1 if user_data.get('hypermarket_store_type', {}).get('has_digital_screens', False) else 0,
|
||||||
|
'J40': user_data.get('hypermarket_store_type', {}).get('screen_count', 0),
|
||||||
|
'K40': user_data.get('hypermarket_store_type', {}).get('screen_percentage', 0),
|
||||||
|
# Convert boolean to 1/0 for has_in_store_radio
|
||||||
|
'M40': 1 if user_data.get('hypermarket_store_type', {}).get('has_in_store_radio', False) else 0,
|
||||||
|
'N40': user_data.get('hypermarket_store_type', {}).get('radio_percentage', 0),
|
||||||
|
|
||||||
|
# On-site channels
|
||||||
|
'B43': user_data.get('website_visitors', 0),
|
||||||
|
'B44': user_data.get('app_users', 0),
|
||||||
|
'B45': user_data.get('loyalty_users', 0),
|
||||||
|
|
||||||
|
# Off-site channels
|
||||||
|
'B49': user_data.get('facebook_followers', 0),
|
||||||
|
'B50': user_data.get('instagram_followers', 0),
|
||||||
|
'B51': user_data.get('google_views', 0),
|
||||||
|
'B52': user_data.get('email_subscribers', 0),
|
||||||
|
'B53': user_data.get('sms_users', 0),
|
||||||
|
'B54': user_data.get('whatsapp_contacts', 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update the cells
|
||||||
|
for cell_ref, value in cell_mappings.items():
|
||||||
|
try:
|
||||||
|
# Force the value to be set, even if the cell is protected or has data validation
|
||||||
|
cell = sheet[cell_ref]
|
||||||
|
cell.value = value
|
||||||
|
print(f"Updated {cell_ref} with value: {value}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating cell {cell_ref}: {e}")
|
||||||
|
|
||||||
|
# Save the workbook with variables updated
|
||||||
|
print("Saving workbook with updated variables...")
|
||||||
|
wb.save(excel_path)
|
||||||
|
|
||||||
|
# Get the calculated years array from config
|
||||||
|
starting_date = user_data.get('starting_date', '')
|
||||||
|
duration = user_data.get('duration', 36)
|
||||||
|
calculated_years = []
|
||||||
|
|
||||||
|
# Import datetime at the module level to avoid scope issues
|
||||||
|
import datetime
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
# Calculate years array based on starting_date and duration
|
||||||
|
try:
|
||||||
|
# Try to parse the date, supporting both dd/mm/yyyy and dd.mm.yyyy formats
|
||||||
|
if starting_date:
|
||||||
|
if '/' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('/'))
|
||||||
|
elif '.' in str(starting_date):
|
||||||
|
day, month, year = map(int, str(starting_date).split('.'))
|
||||||
|
elif '-' in str(starting_date):
|
||||||
|
# Handle ISO format (yyyy-mm-dd)
|
||||||
|
date_parts = str(starting_date).split('-')
|
||||||
|
if len(date_parts) == 3:
|
||||||
|
year, month, day = map(int, date_parts)
|
||||||
|
else:
|
||||||
|
# Default to current date if format is not recognized
|
||||||
|
current_date = datetime.datetime.now()
|
||||||
|
year, month, day = current_date.year, current_date.month, current_date.day
|
||||||
|
elif isinstance(starting_date, datetime.datetime):
|
||||||
|
day, month, year = starting_date.day, starting_date.month, starting_date.year
|
||||||
|
else:
|
||||||
|
# Default to current date if format is not recognized
|
||||||
|
current_date = datetime.datetime.now()
|
||||||
|
year, month, day = current_date.year, current_date.month, current_date.day
|
||||||
|
|
||||||
|
# Create datetime object for starting date
|
||||||
|
start_date = datetime.datetime(year, month, day)
|
||||||
|
|
||||||
|
# Calculate end date (starting date + duration months - 1 day)
|
||||||
|
end_date = start_date + relativedelta(months=duration-1)
|
||||||
|
|
||||||
|
# Create a set of years (to avoid duplicates)
|
||||||
|
years_set = set()
|
||||||
|
|
||||||
|
# Add starting year
|
||||||
|
years_set.add(start_date.year)
|
||||||
|
|
||||||
|
# Add ending year
|
||||||
|
years_set.add(end_date.year)
|
||||||
|
|
||||||
|
# If there are years in between, add those too
|
||||||
|
for y in range(start_date.year + 1, end_date.year):
|
||||||
|
years_set.add(y)
|
||||||
|
|
||||||
|
# Convert set to sorted list
|
||||||
|
calculated_years = sorted(list(years_set))
|
||||||
|
print(f"Calculated years for sheet visibility: {calculated_years}")
|
||||||
|
else:
|
||||||
|
# Default to current year if no starting date
|
||||||
|
calculated_years = [datetime.datetime.now().year]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error calculating years for sheet visibility: {e}")
|
||||||
|
calculated_years = [datetime.datetime.now().year]
|
||||||
|
|
||||||
|
# Hide forecast sheets that aren't in the calculated years array
|
||||||
|
# No sheet renaming - just check existing sheet names
|
||||||
|
for sheet_name in wb.sheetnames:
|
||||||
|
# Check if this is a forecast sheet
|
||||||
|
# Forecast sheets have names like "2025 – Forecast"
|
||||||
|
if "Forecast" in sheet_name:
|
||||||
|
# Extract the year from the sheet name
|
||||||
|
try:
|
||||||
|
sheet_year = int(sheet_name.split()[0])
|
||||||
|
# Hide the sheet if its year is not in the calculated years
|
||||||
|
if sheet_year not in calculated_years:
|
||||||
|
sheet_obj = wb[sheet_name]
|
||||||
|
sheet_obj.sheet_state = 'hidden'
|
||||||
|
print(f"Hiding sheet '{sheet_name}' as year {sheet_year} is not in calculated years {calculated_years}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error extracting year from sheet name '{sheet_name}': {e}")
|
||||||
|
|
||||||
|
# Save the workbook with updated variables and hidden sheets
|
||||||
|
print("Saving workbook with all updates...")
|
||||||
|
wb.save(excel_path)
|
||||||
|
|
||||||
|
print(f"Excel file updated successfully: {excel_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating Excel file: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# For testing purposes
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
excel_path = sys.argv[1]
|
||||||
|
update_excel_variables(excel_path)
|
||||||
|
else:
|
||||||
|
print("Please provide the path to the Excel file as an argument")
|
||||||
247
venv/bin/Activate.ps1
Normal file
247
venv/bin/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Activate a Python virtual environment for the current PowerShell session.
|
||||||
|
|
||||||
|
.Description
|
||||||
|
Pushes the python executable for a virtual environment to the front of the
|
||||||
|
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||||
|
in a Python virtual environment. Makes use of the command line switches as
|
||||||
|
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||||
|
|
||||||
|
.Parameter VenvDir
|
||||||
|
Path to the directory that contains the virtual environment to activate. The
|
||||||
|
default value for this is the parent of the directory that the Activate.ps1
|
||||||
|
script is located within.
|
||||||
|
|
||||||
|
.Parameter Prompt
|
||||||
|
The prompt prefix to display when this virtual environment is activated. By
|
||||||
|
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||||
|
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Verbose
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and shows extra information about the activation as it executes.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||||
|
Activates the Python virtual environment located in the specified location.
|
||||||
|
|
||||||
|
.Example
|
||||||
|
Activate.ps1 -Prompt "MyPython"
|
||||||
|
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||||
|
and prefixes the current prompt with the specified string (surrounded in
|
||||||
|
parentheses) while the virtual environment is active.
|
||||||
|
|
||||||
|
.Notes
|
||||||
|
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||||
|
execution policy for the user. You can do this by issuing the following PowerShell
|
||||||
|
command:
|
||||||
|
|
||||||
|
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
|
||||||
|
For more information on Execution Policies:
|
||||||
|
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||||
|
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$VenvDir,
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[String]
|
||||||
|
$Prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
<# Function declarations --------------------------------------------------- #>
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Synopsis
|
||||||
|
Remove all shell session elements added by the Activate script, including the
|
||||||
|
addition of the virtual environment's Python executable from the beginning of
|
||||||
|
the PATH variable.
|
||||||
|
|
||||||
|
.Parameter NonDestructive
|
||||||
|
If present, do not remove this function from the global namespace for the
|
||||||
|
session.
|
||||||
|
|
||||||
|
#>
|
||||||
|
function global:deactivate ([switch]$NonDestructive) {
|
||||||
|
# Revert to original values
|
||||||
|
|
||||||
|
# The prior prompt:
|
||||||
|
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||||
|
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||||
|
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PYTHONHOME:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# The prior PATH:
|
||||||
|
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||||
|
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||||
|
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the VIRTUAL_ENV altogether:
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||||
|
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||||
|
}
|
||||||
|
|
||||||
|
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||||
|
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||||
|
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
# Leave deactivate function in the global namespace if requested:
|
||||||
|
if (-not $NonDestructive) {
|
||||||
|
Remove-Item -Path function:deactivate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<#
|
||||||
|
.Description
|
||||||
|
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||||
|
given folder, and returns them in a map.
|
||||||
|
|
||||||
|
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||||
|
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||||
|
then it is considered a `key = value` line. The left hand string is the key,
|
||||||
|
the right hand is the value.
|
||||||
|
|
||||||
|
If the value starts with a `'` or a `"` then the first and last character is
|
||||||
|
stripped from the value before being captured.
|
||||||
|
|
||||||
|
.Parameter ConfigDir
|
||||||
|
Path to the directory that contains the `pyvenv.cfg` file.
|
||||||
|
#>
|
||||||
|
function Get-PyVenvConfig(
|
||||||
|
[String]
|
||||||
|
$ConfigDir
|
||||||
|
) {
|
||||||
|
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||||
|
|
||||||
|
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||||
|
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||||
|
|
||||||
|
# An empty map will be returned if no config file is found.
|
||||||
|
$pyvenvConfig = @{ }
|
||||||
|
|
||||||
|
if ($pyvenvConfigPath) {
|
||||||
|
|
||||||
|
Write-Verbose "File exists, parse `key = value` lines"
|
||||||
|
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||||
|
|
||||||
|
$pyvenvConfigContent | ForEach-Object {
|
||||||
|
$keyval = $PSItem -split "\s*=\s*", 2
|
||||||
|
if ($keyval[0] -and $keyval[1]) {
|
||||||
|
$val = $keyval[1]
|
||||||
|
|
||||||
|
# Remove extraneous quotations around a string value.
|
||||||
|
if ("'""".Contains($val.Substring(0, 1))) {
|
||||||
|
$val = $val.Substring(1, $val.Length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
$pyvenvConfig[$keyval[0]] = $val
|
||||||
|
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $pyvenvConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<# Begin Activate script --------------------------------------------------- #>
|
||||||
|
|
||||||
|
# Determine the containing directory of this script
|
||||||
|
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||||
|
|
||||||
|
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||||
|
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||||
|
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||||
|
|
||||||
|
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||||
|
# First, get the location of the virtual environment, it might not be
|
||||||
|
# VenvExecDir if specified on the command line.
|
||||||
|
if ($VenvDir) {
|
||||||
|
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||||
|
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||||
|
Write-Verbose "VenvDir=$VenvDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||||
|
# as `prompt`.
|
||||||
|
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||||
|
|
||||||
|
# Next, set the prompt from the command line, or the config file, or
|
||||||
|
# just use the name of the virtual environment folder.
|
||||||
|
if ($Prompt) {
|
||||||
|
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||||
|
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||||
|
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||||
|
$Prompt = $pyvenvCfg['prompt'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||||
|
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||||
|
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Prompt = '$Prompt'"
|
||||||
|
Write-Verbose "VenvDir='$VenvDir'"
|
||||||
|
|
||||||
|
# Deactivate any currently active virtual environment, but leave the
|
||||||
|
# deactivate function in place.
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||||
|
# that there is an activated venv.
|
||||||
|
$env:VIRTUAL_ENV = $VenvDir
|
||||||
|
|
||||||
|
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
|
||||||
|
Write-Verbose "Setting prompt to '$Prompt'"
|
||||||
|
|
||||||
|
# Set the prompt to include the env name
|
||||||
|
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||||
|
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||||
|
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||||
|
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||||
|
_OLD_VIRTUAL_PROMPT
|
||||||
|
}
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clear PYTHONHOME
|
||||||
|
if (Test-Path -Path Env:PYTHONHOME) {
|
||||||
|
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
Remove-Item -Path Env:PYTHONHOME
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add the venv to the PATH
|
||||||
|
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||||
|
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||||
70
venv/bin/activate
Normal file
70
venv/bin/activate
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# You cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
# on Windows, a path can contain colons and backslashes and has to be converted:
|
||||||
|
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
|
||||||
|
# transform D:\path\to\venv to /d/path/to/venv on MSYS
|
||||||
|
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||||
|
export VIRTUAL_ENV=$(cygpath /home/pixot/business_case_form/venv)
|
||||||
|
else
|
||||||
|
# use the path as-is
|
||||||
|
export VIRTUAL_ENV=/home/pixot/business_case_form/venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
PS1='(venv) '"${PS1:-}"
|
||||||
|
export PS1
|
||||||
|
VIRTUAL_ENV_PROMPT='(venv) '
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call hash to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
hash -r 2> /dev/null
|
||||||
27
venv/bin/activate.csh
Normal file
27
venv/bin/activate.csh
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV /home/pixot/business_case_form/venv
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
set prompt = '(venv) '"$prompt"
|
||||||
|
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
69
venv/bin/activate.fish
Normal file
69
venv/bin/activate.fish
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||||
|
# (https://fishshell.com/). You cannot run it directly.
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
# prevents error when using nested fish instances (Issue #93858)
|
||||||
|
if functions -q _old_fish_prompt
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV /home/pixot/business_case_form/venv
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||||
|
|
||||||
|
# Unset PYTHONHOME if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# With the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command.
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
|
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
# Output the original/"old" prompt.
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||||
|
end
|
||||||
8
venv/bin/pip
Executable file
8
venv/bin/pip
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/pixot/business_case_form/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
venv/bin/pip3
Executable file
8
venv/bin/pip3
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/pixot/business_case_form/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
8
venv/bin/pip3.12
Executable file
8
venv/bin/pip3.12
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/home/pixot/business_case_form/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
1
venv/bin/python
Symbolic link
1
venv/bin/python
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
1
venv/bin/python3
Symbolic link
1
venv/bin/python3
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
1
venv/bin/python3.12
Symbolic link
1
venv/bin/python3.12
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
python3
|
||||||
79
venv/bin/vba_extract.py
Executable file
79
venv/bin/vba_extract.py
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/home/pixot/business_case_form/venv/bin/python3
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# vba_extract - A simple utility to extract a vbaProject.bin binary from an
|
||||||
|
# Excel 2007+ xlsm file for insertion into an XlsxWriter file.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-2-Clause
|
||||||
|
#
|
||||||
|
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
||||||
|
#
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from zipfile import BadZipFile, ZipFile
|
||||||
|
|
||||||
|
|
||||||
|
def extract_file(xlsm_zip, filename):
|
||||||
|
# Extract a single file from an Excel xlsm macro file.
|
||||||
|
data = xlsm_zip.read("xl/" + filename)
|
||||||
|
|
||||||
|
# Write the data to a local file.
|
||||||
|
file = open(filename, "wb")
|
||||||
|
file.write(data)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
|
||||||
|
# The VBA project file and project signature file we want to extract.
|
||||||
|
vba_filename = "vbaProject.bin"
|
||||||
|
vba_signature_filename = "vbaProjectSignature.bin"
|
||||||
|
|
||||||
|
# Get the xlsm file name from the commandline.
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
xlsm_file = sys.argv[1]
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"\nUtility to extract a vbaProject.bin binary from an Excel 2007+ "
|
||||||
|
"xlsm macro file for insertion into an XlsxWriter file.\n"
|
||||||
|
"If the macros are digitally signed, extracts also a vbaProjectSignature.bin "
|
||||||
|
"file.\n"
|
||||||
|
"\n"
|
||||||
|
"See: https://xlsxwriter.readthedocs.io/working_with_macros.html\n"
|
||||||
|
"\n"
|
||||||
|
"Usage: vba_extract file.xlsm\n"
|
||||||
|
)
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Open the Excel xlsm file as a zip file.
|
||||||
|
xlsm_zip = ZipFile(xlsm_file, "r")
|
||||||
|
|
||||||
|
# Read the xl/vbaProject.bin file.
|
||||||
|
extract_file(xlsm_zip, vba_filename)
|
||||||
|
print(f"Extracted: {vba_filename}")
|
||||||
|
|
||||||
|
if "xl/" + vba_signature_filename in xlsm_zip.namelist():
|
||||||
|
extract_file(xlsm_zip, vba_signature_filename)
|
||||||
|
print(f"Extracted: {vba_signature_filename}")
|
||||||
|
|
||||||
|
|
||||||
|
except IOError as e:
|
||||||
|
print(f"File error: {str(e)}")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
# Usually when there isn't a xl/vbaProject.bin member in the file.
|
||||||
|
print(f"File error: {str(e)}")
|
||||||
|
print(f"File may not be an Excel xlsm macro file: '{xlsm_file}'")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
except BadZipFile as e:
|
||||||
|
# Usually if the file is an xls file and not an xlsm file.
|
||||||
|
print(f"File error: {str(e)}: '{xlsm_file}'")
|
||||||
|
print("File may not be an Excel xlsm macro file.")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Catch any other exceptions.
|
||||||
|
print(f"File error: {str(e)}")
|
||||||
|
sys.exit()
|
||||||
24
venv/lib/python3.12/site-packages/dateutil/__init__.py
Normal file
24
venv/lib/python3.12/site-packages/dateutil/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ._version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = 'unknown'
|
||||||
|
|
||||||
|
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||||
|
'utils', 'zoneinfo']
|
||||||
|
|
||||||
|
def __getattr__(name):
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
if name in __all__:
|
||||||
|
return importlib.import_module("." + name, __name__)
|
||||||
|
raise AttributeError(
|
||||||
|
"module {!r} has not attribute {!r}".format(__name__, name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def __dir__():
|
||||||
|
# __dir__ should include all the lazy-importable modules as well.
|
||||||
|
return [x for x in globals() if x not in sys.modules] + __all__
|
||||||
43
venv/lib/python3.12/site-packages/dateutil/_common.py
Normal file
43
venv/lib/python3.12/site-packages/dateutil/_common.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Common code used in multiple modules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class weekday(object):
|
||||||
|
__slots__ = ["weekday", "n"]
|
||||||
|
|
||||||
|
def __init__(self, weekday, n=None):
|
||||||
|
self.weekday = weekday
|
||||||
|
self.n = n
|
||||||
|
|
||||||
|
def __call__(self, n):
|
||||||
|
if n == self.n:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return self.__class__(self.weekday, n)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
try:
|
||||||
|
if self.weekday != other.weekday or self.n != other.n:
|
||||||
|
return False
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((
|
||||||
|
self.weekday,
|
||||||
|
self.n,
|
||||||
|
))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||||
|
if not self.n:
|
||||||
|
return s
|
||||||
|
else:
|
||||||
|
return "%s(%+d)" % (s, self.n)
|
||||||
|
|
||||||
|
# vim:ts=4:sw=4:et
|
||||||
4
venv/lib/python3.12/site-packages/dateutil/_version.py
Normal file
4
venv/lib/python3.12/site-packages/dateutil/_version.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# file generated by setuptools_scm
|
||||||
|
# don't change, don't track in version control
|
||||||
|
__version__ = version = '2.9.0.post0'
|
||||||
|
__version_tuple__ = version_tuple = (2, 9, 0)
|
||||||
89
venv/lib/python3.12/site-packages/dateutil/easter.py
Normal file
89
venv/lib/python3.12/site-packages/dateutil/easter.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers a generic Easter computing method for any given year, using
|
||||||
|
Western, Orthodox or Julian algorithms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
||||||
|
|
||||||
|
EASTER_JULIAN = 1
|
||||||
|
EASTER_ORTHODOX = 2
|
||||||
|
EASTER_WESTERN = 3
|
||||||
|
|
||||||
|
|
||||||
|
def easter(year, method=EASTER_WESTERN):
|
||||||
|
"""
|
||||||
|
This method was ported from the work done by GM Arts,
|
||||||
|
on top of the algorithm by Claus Tondering, which was
|
||||||
|
based in part on the algorithm of Ouding (1940), as
|
||||||
|
quoted in "Explanatory Supplement to the Astronomical
|
||||||
|
Almanac", P. Kenneth Seidelmann, editor.
|
||||||
|
|
||||||
|
This algorithm implements three different Easter
|
||||||
|
calculation methods:
|
||||||
|
|
||||||
|
1. Original calculation in Julian calendar, valid in
|
||||||
|
dates after 326 AD
|
||||||
|
2. Original method, with date converted to Gregorian
|
||||||
|
calendar, valid in years 1583 to 4099
|
||||||
|
3. Revised method, in Gregorian calendar, valid in
|
||||||
|
years 1583 to 4099 as well
|
||||||
|
|
||||||
|
These methods are represented by the constants:
|
||||||
|
|
||||||
|
* ``EASTER_JULIAN = 1``
|
||||||
|
* ``EASTER_ORTHODOX = 2``
|
||||||
|
* ``EASTER_WESTERN = 3``
|
||||||
|
|
||||||
|
The default method is method 3.
|
||||||
|
|
||||||
|
More about the algorithm may be found at:
|
||||||
|
|
||||||
|
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (1 <= method <= 3):
|
||||||
|
raise ValueError("invalid method")
|
||||||
|
|
||||||
|
# g - Golden year - 1
|
||||||
|
# c - Century
|
||||||
|
# h - (23 - Epact) mod 30
|
||||||
|
# i - Number of days from March 21 to Paschal Full Moon
|
||||||
|
# j - Weekday for PFM (0=Sunday, etc)
|
||||||
|
# p - Number of days from March 21 to Sunday on or before PFM
|
||||||
|
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
||||||
|
# e - Extra days to add for method 2 (converting Julian
|
||||||
|
# date to Gregorian date)
|
||||||
|
|
||||||
|
y = year
|
||||||
|
g = y % 19
|
||||||
|
e = 0
|
||||||
|
if method < 3:
|
||||||
|
# Old method
|
||||||
|
i = (19*g + 15) % 30
|
||||||
|
j = (y + y//4 + i) % 7
|
||||||
|
if method == 2:
|
||||||
|
# Extra dates to convert Julian to Gregorian date
|
||||||
|
e = 10
|
||||||
|
if y > 1600:
|
||||||
|
e = e + y//100 - 16 - (y//100 - 16)//4
|
||||||
|
else:
|
||||||
|
# New method
|
||||||
|
c = y//100
|
||||||
|
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
||||||
|
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
||||||
|
j = (y + y//4 + i + 2 - c + c//4) % 7
|
||||||
|
|
||||||
|
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
||||||
|
# (later dates apply to method 2, although 23 May never actually occurs)
|
||||||
|
p = i - j + e
|
||||||
|
d = 1 + (p + 27 + (p + 6)//40) % 31
|
||||||
|
m = 3 + (p + 26)//30
|
||||||
|
return datetime.date(int(y), int(m), int(d))
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from ._parser import parse, parser, parserinfo, ParserError
|
||||||
|
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
||||||
|
from ._parser import UnknownTimezoneWarning
|
||||||
|
|
||||||
|
from ._parser import __doc__
|
||||||
|
|
||||||
|
from .isoparser import isoparser, isoparse
|
||||||
|
|
||||||
|
__all__ = ['parse', 'parser', 'parserinfo',
|
||||||
|
'isoparse', 'isoparser',
|
||||||
|
'ParserError',
|
||||||
|
'UnknownTimezoneWarning']
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Deprecate portions of the private interface so that downstream code that
|
||||||
|
# is improperly relying on it is given *some* notice.
|
||||||
|
|
||||||
|
|
||||||
|
def __deprecated_private_func(f):
|
||||||
|
from functools import wraps
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private function and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=f.__name__)
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def deprecated_func(*args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return deprecated_func
|
||||||
|
|
||||||
|
def __deprecate_private_class(c):
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private class and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=c.__name__)
|
||||||
|
|
||||||
|
class private_class(c):
|
||||||
|
__doc__ = c.__doc__
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
super(private_class, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
private_class.__name__ = c.__name__
|
||||||
|
|
||||||
|
return private_class
|
||||||
|
|
||||||
|
|
||||||
|
from ._parser import _timelex, _resultbase
|
||||||
|
from ._parser import _tzparser, _parsetz
|
||||||
|
|
||||||
|
_timelex = __deprecate_private_class(_timelex)
|
||||||
|
_tzparser = __deprecate_private_class(_tzparser)
|
||||||
|
_resultbase = __deprecate_private_class(_resultbase)
|
||||||
|
_parsetz = __deprecated_private_func(_parsetz)
|
||||||
1613
venv/lib/python3.12/site-packages/dateutil/parser/_parser.py
Normal file
1613
venv/lib/python3.12/site-packages/dateutil/parser/_parser.py
Normal file
File diff suppressed because it is too large
Load Diff
416
venv/lib/python3.12/site-packages/dateutil/parser/isoparser.py
Normal file
416
venv/lib/python3.12/site-packages/dateutil/parser/isoparser.py
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers a parser for ISO-8601 strings
|
||||||
|
|
||||||
|
It is intended to support all valid date, time and datetime formats per the
|
||||||
|
ISO-8601 specification.
|
||||||
|
|
||||||
|
..versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta, time, date
|
||||||
|
import calendar
|
||||||
|
from dateutil import tz
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import re
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = ["isoparse", "isoparser"]
|
||||||
|
|
||||||
|
|
||||||
|
def _takes_ascii(f):
|
||||||
|
@wraps(f)
|
||||||
|
def func(self, str_in, *args, **kwargs):
|
||||||
|
# If it's a stream, read the whole thing
|
||||||
|
str_in = getattr(str_in, 'read', lambda: str_in)()
|
||||||
|
|
||||||
|
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
||||||
|
if isinstance(str_in, six.text_type):
|
||||||
|
# ASCII is the same in UTF-8
|
||||||
|
try:
|
||||||
|
str_in = str_in.encode('ascii')
|
||||||
|
except UnicodeEncodeError as e:
|
||||||
|
msg = 'ISO-8601 strings should contain only ASCII characters'
|
||||||
|
six.raise_from(ValueError(msg), e)
|
||||||
|
|
||||||
|
return f(self, str_in, *args, **kwargs)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
class isoparser(object):
|
||||||
|
def __init__(self, sep=None):
|
||||||
|
"""
|
||||||
|
:param sep:
|
||||||
|
A single character that separates date and time portions. If
|
||||||
|
``None``, the parser will accept any single character.
|
||||||
|
For strict ISO-8601 adherence, pass ``'T'``.
|
||||||
|
"""
|
||||||
|
if sep is not None:
|
||||||
|
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
||||||
|
raise ValueError('Separator must be a single, non-numeric ' +
|
||||||
|
'ASCII character')
|
||||||
|
|
||||||
|
sep = sep.encode('ascii')
|
||||||
|
|
||||||
|
self._sep = sep
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def isoparse(self, dt_str):
|
||||||
|
"""
|
||||||
|
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
||||||
|
|
||||||
|
An ISO-8601 datetime string consists of a date portion, followed
|
||||||
|
optionally by a time portion - the date and time portions are separated
|
||||||
|
by a single character separator, which is ``T`` in the official
|
||||||
|
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
||||||
|
combined with a time portion.
|
||||||
|
|
||||||
|
Supported date formats are:
|
||||||
|
|
||||||
|
Common:
|
||||||
|
|
||||||
|
- ``YYYY``
|
||||||
|
- ``YYYY-MM``
|
||||||
|
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
||||||
|
|
||||||
|
Uncommon:
|
||||||
|
|
||||||
|
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
||||||
|
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
||||||
|
|
||||||
|
The ISO week and day numbering follows the same logic as
|
||||||
|
:func:`datetime.date.isocalendar`.
|
||||||
|
|
||||||
|
Supported time formats are:
|
||||||
|
|
||||||
|
- ``hh``
|
||||||
|
- ``hh:mm`` or ``hhmm``
|
||||||
|
- ``hh:mm:ss`` or ``hhmmss``
|
||||||
|
- ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits)
|
||||||
|
|
||||||
|
Midnight is a special case for `hh`, as the standard supports both
|
||||||
|
00:00 and 24:00 as a representation. The decimal separator can be
|
||||||
|
either a dot or a comma.
|
||||||
|
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
Support for fractional components other than seconds is part of the
|
||||||
|
ISO-8601 standard, but is not currently implemented in this parser.
|
||||||
|
|
||||||
|
Supported time zone offset formats are:
|
||||||
|
|
||||||
|
- `Z` (UTC)
|
||||||
|
- `±HH:MM`
|
||||||
|
- `±HHMM`
|
||||||
|
- `±HH`
|
||||||
|
|
||||||
|
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
||||||
|
with the exception of UTC, which will be represented as
|
||||||
|
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
||||||
|
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
||||||
|
|
||||||
|
:param dt_str:
|
||||||
|
A string or stream containing only an ISO-8601 datetime string
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.datetime` representing the string.
|
||||||
|
Unspecified components default to their lowest value.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
As of version 2.7.0, the strictness of the parser should not be
|
||||||
|
considered a stable part of the contract. Any valid ISO-8601 string
|
||||||
|
that parses correctly with the default settings will continue to
|
||||||
|
parse correctly in future versions, but invalid strings that
|
||||||
|
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
|
||||||
|
guaranteed to continue failing in future versions if they encode
|
||||||
|
a valid date.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
components, pos = self._parse_isodate(dt_str)
|
||||||
|
|
||||||
|
if len(dt_str) > pos:
|
||||||
|
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
||||||
|
components += self._parse_isotime(dt_str[pos + 1:])
|
||||||
|
else:
|
||||||
|
raise ValueError('String contains unknown ISO components')
|
||||||
|
|
||||||
|
if len(components) > 3 and components[3] == 24:
|
||||||
|
components[3] = 0
|
||||||
|
return datetime(*components) + timedelta(days=1)
|
||||||
|
|
||||||
|
return datetime(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_isodate(self, datestr):
|
||||||
|
"""
|
||||||
|
Parse the date portion of an ISO string.
|
||||||
|
|
||||||
|
:param datestr:
|
||||||
|
The string portion of an ISO string, without a separator
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.date` object
|
||||||
|
"""
|
||||||
|
components, pos = self._parse_isodate(datestr)
|
||||||
|
if pos < len(datestr):
|
||||||
|
raise ValueError('String contains unknown ISO ' +
|
||||||
|
'components: {!r}'.format(datestr.decode('ascii')))
|
||||||
|
return date(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_isotime(self, timestr):
|
||||||
|
"""
|
||||||
|
Parse the time portion of an ISO string.
|
||||||
|
|
||||||
|
:param timestr:
|
||||||
|
The time portion of an ISO string, without a separator
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.time` object
|
||||||
|
"""
|
||||||
|
components = self._parse_isotime(timestr)
|
||||||
|
if components[0] == 24:
|
||||||
|
components[0] = 0
|
||||||
|
return time(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||||
|
"""
|
||||||
|
Parse a valid ISO time zone string.
|
||||||
|
|
||||||
|
See :func:`isoparser.isoparse` for details on supported formats.
|
||||||
|
|
||||||
|
:param tzstr:
|
||||||
|
A string representing an ISO time zone offset
|
||||||
|
|
||||||
|
:param zero_as_utc:
|
||||||
|
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
||||||
|
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
||||||
|
specified) offsets equivalent to UTC.
|
||||||
|
"""
|
||||||
|
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
_DATE_SEP = b'-'
|
||||||
|
_TIME_SEP = b':'
|
||||||
|
_FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
|
||||||
|
|
||||||
|
def _parse_isodate(self, dt_str):
|
||||||
|
try:
|
||||||
|
return self._parse_isodate_common(dt_str)
|
||||||
|
except ValueError:
|
||||||
|
return self._parse_isodate_uncommon(dt_str)
|
||||||
|
|
||||||
|
def _parse_isodate_common(self, dt_str):
|
||||||
|
len_str = len(dt_str)
|
||||||
|
components = [1, 1, 1]
|
||||||
|
|
||||||
|
if len_str < 4:
|
||||||
|
raise ValueError('ISO string too short')
|
||||||
|
|
||||||
|
# Year
|
||||||
|
components[0] = int(dt_str[0:4])
|
||||||
|
pos = 4
|
||||||
|
if pos >= len_str:
|
||||||
|
return components, pos
|
||||||
|
|
||||||
|
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
||||||
|
if has_sep:
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Month
|
||||||
|
if len_str - pos < 2:
|
||||||
|
raise ValueError('Invalid common month')
|
||||||
|
|
||||||
|
components[1] = int(dt_str[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
if pos >= len_str:
|
||||||
|
if has_sep:
|
||||||
|
return components, pos
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid ISO format')
|
||||||
|
|
||||||
|
if has_sep:
|
||||||
|
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
||||||
|
raise ValueError('Invalid separator in ISO string')
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Day
|
||||||
|
if len_str - pos < 2:
|
||||||
|
raise ValueError('Invalid common day')
|
||||||
|
components[2] = int(dt_str[pos:pos + 2])
|
||||||
|
return components, pos + 2
|
||||||
|
|
||||||
|
def _parse_isodate_uncommon(self, dt_str):
|
||||||
|
if len(dt_str) < 4:
|
||||||
|
raise ValueError('ISO string too short')
|
||||||
|
|
||||||
|
# All ISO formats start with the year
|
||||||
|
year = int(dt_str[0:4])
|
||||||
|
|
||||||
|
has_sep = dt_str[4:5] == self._DATE_SEP
|
||||||
|
|
||||||
|
pos = 4 + has_sep # Skip '-' if it's there
|
||||||
|
if dt_str[pos:pos + 1] == b'W':
|
||||||
|
# YYYY-?Www-?D?
|
||||||
|
pos += 1
|
||||||
|
weekno = int(dt_str[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
dayno = 1
|
||||||
|
if len(dt_str) > pos:
|
||||||
|
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
||||||
|
raise ValueError('Inconsistent use of dash separator')
|
||||||
|
|
||||||
|
pos += has_sep
|
||||||
|
|
||||||
|
dayno = int(dt_str[pos:pos + 1])
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
base_date = self._calculate_weekdate(year, weekno, dayno)
|
||||||
|
else:
|
||||||
|
# YYYYDDD or YYYY-DDD
|
||||||
|
if len(dt_str) - pos < 3:
|
||||||
|
raise ValueError('Invalid ordinal day')
|
||||||
|
|
||||||
|
ordinal_day = int(dt_str[pos:pos + 3])
|
||||||
|
pos += 3
|
||||||
|
|
||||||
|
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
||||||
|
raise ValueError('Invalid ordinal day' +
|
||||||
|
' {} for year {}'.format(ordinal_day, year))
|
||||||
|
|
||||||
|
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
||||||
|
|
||||||
|
components = [base_date.year, base_date.month, base_date.day]
|
||||||
|
return components, pos
|
||||||
|
|
||||||
|
def _calculate_weekdate(self, year, week, day):
|
||||||
|
"""
|
||||||
|
Calculate the day of corresponding to the ISO year-week-day calendar.
|
||||||
|
|
||||||
|
This function is effectively the inverse of
|
||||||
|
:func:`datetime.date.isocalendar`.
|
||||||
|
|
||||||
|
:param year:
|
||||||
|
The year in the ISO calendar
|
||||||
|
|
||||||
|
:param week:
|
||||||
|
The week in the ISO calendar - range is [1, 53]
|
||||||
|
|
||||||
|
:param day:
|
||||||
|
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.date`
|
||||||
|
"""
|
||||||
|
if not 0 < week < 54:
|
||||||
|
raise ValueError('Invalid week: {}'.format(week))
|
||||||
|
|
||||||
|
if not 0 < day < 8: # Range is 1-7
|
||||||
|
raise ValueError('Invalid weekday: {}'.format(day))
|
||||||
|
|
||||||
|
# Get week 1 for the specific year:
|
||||||
|
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
||||||
|
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
||||||
|
|
||||||
|
# Now add the specific number of weeks and days to get what we want
|
||||||
|
week_offset = (week - 1) * 7 + (day - 1)
|
||||||
|
return week_1 + timedelta(days=week_offset)
|
||||||
|
|
||||||
|
def _parse_isotime(self, timestr):
|
||||||
|
len_str = len(timestr)
|
||||||
|
components = [0, 0, 0, 0, None]
|
||||||
|
pos = 0
|
||||||
|
comp = -1
|
||||||
|
|
||||||
|
if len_str < 2:
|
||||||
|
raise ValueError('ISO time too short')
|
||||||
|
|
||||||
|
has_sep = False
|
||||||
|
|
||||||
|
while pos < len_str and comp < 5:
|
||||||
|
comp += 1
|
||||||
|
|
||||||
|
if timestr[pos:pos + 1] in b'-+Zz':
|
||||||
|
# Detect time zone boundary
|
||||||
|
components[-1] = self._parse_tzstr(timestr[pos:])
|
||||||
|
pos = len_str
|
||||||
|
break
|
||||||
|
|
||||||
|
if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP:
|
||||||
|
has_sep = True
|
||||||
|
pos += 1
|
||||||
|
elif comp == 2 and has_sep:
|
||||||
|
if timestr[pos:pos+1] != self._TIME_SEP:
|
||||||
|
raise ValueError('Inconsistent use of colon separator')
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if comp < 3:
|
||||||
|
# Hour, minute, second
|
||||||
|
components[comp] = int(timestr[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
if comp == 3:
|
||||||
|
# Fraction of a second
|
||||||
|
frac = self._FRACTION_REGEX.match(timestr[pos:])
|
||||||
|
if not frac:
|
||||||
|
continue
|
||||||
|
|
||||||
|
us_str = frac.group(1)[:6] # Truncate to microseconds
|
||||||
|
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
||||||
|
pos += len(frac.group())
|
||||||
|
|
||||||
|
if pos < len_str:
|
||||||
|
raise ValueError('Unused components in ISO string')
|
||||||
|
|
||||||
|
if components[0] == 24:
|
||||||
|
# Standard supports 00:00 and 24:00 as representations of midnight
|
||||||
|
if any(component != 0 for component in components[1:4]):
|
||||||
|
raise ValueError('Hour may only be 24 at 24:00:00.000')
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||||
|
if tzstr == b'Z' or tzstr == b'z':
|
||||||
|
return tz.UTC
|
||||||
|
|
||||||
|
if len(tzstr) not in {3, 5, 6}:
|
||||||
|
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
||||||
|
|
||||||
|
if tzstr[0:1] == b'-':
|
||||||
|
mult = -1
|
||||||
|
elif tzstr[0:1] == b'+':
|
||||||
|
mult = 1
|
||||||
|
else:
|
||||||
|
raise ValueError('Time zone offset requires sign')
|
||||||
|
|
||||||
|
hours = int(tzstr[1:3])
|
||||||
|
if len(tzstr) == 3:
|
||||||
|
minutes = 0
|
||||||
|
else:
|
||||||
|
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
||||||
|
|
||||||
|
if zero_as_utc and hours == 0 and minutes == 0:
|
||||||
|
return tz.UTC
|
||||||
|
else:
|
||||||
|
if minutes > 59:
|
||||||
|
raise ValueError('Invalid minutes in time zone offset')
|
||||||
|
|
||||||
|
if hours > 23:
|
||||||
|
raise ValueError('Invalid hours in time zone offset')
|
||||||
|
|
||||||
|
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ISOPARSER = isoparser()
|
||||||
|
isoparse = DEFAULT_ISOPARSER.isoparse
|
||||||
599
venv/lib/python3.12/site-packages/dateutil/relativedelta.py
Normal file
599
venv/lib/python3.12/site-packages/dateutil/relativedelta.py
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
|
import calendar
|
||||||
|
|
||||||
|
import operator
|
||||||
|
from math import copysign
|
||||||
|
|
||||||
|
from six import integer_types
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from ._common import weekday
|
||||||
|
|
||||||
|
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||||
|
|
||||||
|
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||||
|
|
||||||
|
|
||||||
|
class relativedelta(object):
|
||||||
|
"""
|
||||||
|
The relativedelta type is designed to be applied to an existing datetime and
|
||||||
|
can replace specific components of that datetime, or represents an interval
|
||||||
|
of time.
|
||||||
|
|
||||||
|
It is based on the specification of the excellent work done by M.-A. Lemburg
|
||||||
|
in his
|
||||||
|
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
||||||
|
However, notice that this type does *NOT* implement the same algorithm as
|
||||||
|
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
||||||
|
|
||||||
|
There are two different ways to build a relativedelta instance. The
|
||||||
|
first one is passing it two date/datetime classes::
|
||||||
|
|
||||||
|
relativedelta(datetime1, datetime2)
|
||||||
|
|
||||||
|
The second one is passing it any number of the following keyword arguments::
|
||||||
|
|
||||||
|
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||||
|
|
||||||
|
year, month, day, hour, minute, second, microsecond:
|
||||||
|
Absolute information (argument is singular); adding or subtracting a
|
||||||
|
relativedelta with absolute information does not perform an arithmetic
|
||||||
|
operation, but rather REPLACES the corresponding value in the
|
||||||
|
original datetime with the value(s) in relativedelta.
|
||||||
|
|
||||||
|
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||||
|
Relative information, may be negative (argument is plural); adding
|
||||||
|
or subtracting a relativedelta with relative information performs
|
||||||
|
the corresponding arithmetic operation on the original datetime value
|
||||||
|
with the information in the relativedelta.
|
||||||
|
|
||||||
|
weekday:
|
||||||
|
One of the weekday instances (MO, TU, etc) available in the
|
||||||
|
relativedelta module. These instances may receive a parameter N,
|
||||||
|
specifying the Nth weekday, which could be positive or negative
|
||||||
|
(like MO(+1) or MO(-2)). Not specifying it is the same as specifying
|
||||||
|
+1. You can also use an integer, where 0=MO. This argument is always
|
||||||
|
relative e.g. if the calculated date is already Monday, using MO(1)
|
||||||
|
or MO(-1) won't change the day. To effectively make it absolute, use
|
||||||
|
it in combination with the day argument (e.g. day=1, MO(1) for first
|
||||||
|
Monday of the month).
|
||||||
|
|
||||||
|
leapdays:
|
||||||
|
Will add given days to the date found, if year is a leap
|
||||||
|
year, and the date found is post 28 of february.
|
||||||
|
|
||||||
|
yearday, nlyearday:
|
||||||
|
Set the yearday or the non-leap year day (jump leap days).
|
||||||
|
These are converted to day/month/leapdays information.
|
||||||
|
|
||||||
|
There are relative and absolute forms of the keyword
|
||||||
|
arguments. The plural is relative, and the singular is
|
||||||
|
absolute. For each argument in the order below, the absolute form
|
||||||
|
is applied first (by setting each attribute to that value) and
|
||||||
|
then the relative form (by adding the value to the attribute).
|
||||||
|
|
||||||
|
The order of attributes considered when this relativedelta is
|
||||||
|
added to a datetime is:
|
||||||
|
|
||||||
|
1. Year
|
||||||
|
2. Month
|
||||||
|
3. Day
|
||||||
|
4. Hours
|
||||||
|
5. Minutes
|
||||||
|
6. Seconds
|
||||||
|
7. Microseconds
|
||||||
|
|
||||||
|
Finally, weekday is applied, using the rule described above.
|
||||||
|
|
||||||
|
For example
|
||||||
|
|
||||||
|
>>> from datetime import datetime
|
||||||
|
>>> from dateutil.relativedelta import relativedelta, MO
|
||||||
|
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
||||||
|
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
||||||
|
>>> dt + delta
|
||||||
|
datetime.datetime(2018, 4, 2, 14, 37)
|
||||||
|
|
||||||
|
First, the day is set to 1 (the first of the month), then 25 hours
|
||||||
|
are added, to get to the 2nd day and 14th hour, finally the
|
||||||
|
weekday is applied, but since the 2nd is already a Monday there is
|
||||||
|
no effect.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, dt1=None, dt2=None,
|
||||||
|
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||||
|
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||||
|
year=None, month=None, day=None, weekday=None,
|
||||||
|
yearday=None, nlyearday=None,
|
||||||
|
hour=None, minute=None, second=None, microsecond=None):
|
||||||
|
|
||||||
|
if dt1 and dt2:
|
||||||
|
# datetime is a subclass of date. So both must be date
|
||||||
|
if not (isinstance(dt1, datetime.date) and
|
||||||
|
isinstance(dt2, datetime.date)):
|
||||||
|
raise TypeError("relativedelta only diffs datetime/date")
|
||||||
|
|
||||||
|
# We allow two dates, or two datetimes, so we coerce them to be
|
||||||
|
# of the same type
|
||||||
|
if (isinstance(dt1, datetime.datetime) !=
|
||||||
|
isinstance(dt2, datetime.datetime)):
|
||||||
|
if not isinstance(dt1, datetime.datetime):
|
||||||
|
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||||
|
elif not isinstance(dt2, datetime.datetime):
|
||||||
|
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||||
|
|
||||||
|
self.years = 0
|
||||||
|
self.months = 0
|
||||||
|
self.days = 0
|
||||||
|
self.leapdays = 0
|
||||||
|
self.hours = 0
|
||||||
|
self.minutes = 0
|
||||||
|
self.seconds = 0
|
||||||
|
self.microseconds = 0
|
||||||
|
self.year = None
|
||||||
|
self.month = None
|
||||||
|
self.day = None
|
||||||
|
self.weekday = None
|
||||||
|
self.hour = None
|
||||||
|
self.minute = None
|
||||||
|
self.second = None
|
||||||
|
self.microsecond = None
|
||||||
|
self._has_time = 0
|
||||||
|
|
||||||
|
# Get year / month delta between the two
|
||||||
|
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
||||||
|
self._set_months(months)
|
||||||
|
|
||||||
|
# Remove the year/month delta so the timedelta is just well-defined
|
||||||
|
# time units (seconds, days and microseconds)
|
||||||
|
dtm = self.__radd__(dt2)
|
||||||
|
|
||||||
|
# If we've overshot our target, make an adjustment
|
||||||
|
if dt1 < dt2:
|
||||||
|
compare = operator.gt
|
||||||
|
increment = 1
|
||||||
|
else:
|
||||||
|
compare = operator.lt
|
||||||
|
increment = -1
|
||||||
|
|
||||||
|
while compare(dt1, dtm):
|
||||||
|
months += increment
|
||||||
|
self._set_months(months)
|
||||||
|
dtm = self.__radd__(dt2)
|
||||||
|
|
||||||
|
# Get the timedelta between the "months-adjusted" date and dt1
|
||||||
|
delta = dt1 - dtm
|
||||||
|
self.seconds = delta.seconds + delta.days * 86400
|
||||||
|
self.microseconds = delta.microseconds
|
||||||
|
else:
|
||||||
|
# Check for non-integer values in integer-only quantities
|
||||||
|
if any(x is not None and x != int(x) for x in (years, months)):
|
||||||
|
raise ValueError("Non-integer years and months are "
|
||||||
|
"ambiguous and not currently supported.")
|
||||||
|
|
||||||
|
# Relative information
|
||||||
|
self.years = int(years)
|
||||||
|
self.months = int(months)
|
||||||
|
self.days = days + weeks * 7
|
||||||
|
self.leapdays = leapdays
|
||||||
|
self.hours = hours
|
||||||
|
self.minutes = minutes
|
||||||
|
self.seconds = seconds
|
||||||
|
self.microseconds = microseconds
|
||||||
|
|
||||||
|
# Absolute information
|
||||||
|
self.year = year
|
||||||
|
self.month = month
|
||||||
|
self.day = day
|
||||||
|
self.hour = hour
|
||||||
|
self.minute = minute
|
||||||
|
self.second = second
|
||||||
|
self.microsecond = microsecond
|
||||||
|
|
||||||
|
if any(x is not None and int(x) != x
|
||||||
|
for x in (year, month, day, hour,
|
||||||
|
minute, second, microsecond)):
|
||||||
|
# For now we'll deprecate floats - later it'll be an error.
|
||||||
|
warn("Non-integer value passed as absolute information. " +
|
||||||
|
"This is not a well-defined condition and will raise " +
|
||||||
|
"errors in future versions.", DeprecationWarning)
|
||||||
|
|
||||||
|
if isinstance(weekday, integer_types):
|
||||||
|
self.weekday = weekdays[weekday]
|
||||||
|
else:
|
||||||
|
self.weekday = weekday
|
||||||
|
|
||||||
|
yday = 0
|
||||||
|
if nlyearday:
|
||||||
|
yday = nlyearday
|
||||||
|
elif yearday:
|
||||||
|
yday = yearday
|
||||||
|
if yearday > 59:
|
||||||
|
self.leapdays = -1
|
||||||
|
if yday:
|
||||||
|
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
||||||
|
243, 273, 304, 334, 366]
|
||||||
|
for idx, ydays in enumerate(ydayidx):
|
||||||
|
if yday <= ydays:
|
||||||
|
self.month = idx+1
|
||||||
|
if idx == 0:
|
||||||
|
self.day = yday
|
||||||
|
else:
|
||||||
|
self.day = yday-ydayidx[idx-1]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid year day (%d)" % yday)
|
||||||
|
|
||||||
|
self._fix()
|
||||||
|
|
||||||
|
def _fix(self):
|
||||||
|
if abs(self.microseconds) > 999999:
|
||||||
|
s = _sign(self.microseconds)
|
||||||
|
div, mod = divmod(self.microseconds * s, 1000000)
|
||||||
|
self.microseconds = mod * s
|
||||||
|
self.seconds += div * s
|
||||||
|
if abs(self.seconds) > 59:
|
||||||
|
s = _sign(self.seconds)
|
||||||
|
div, mod = divmod(self.seconds * s, 60)
|
||||||
|
self.seconds = mod * s
|
||||||
|
self.minutes += div * s
|
||||||
|
if abs(self.minutes) > 59:
|
||||||
|
s = _sign(self.minutes)
|
||||||
|
div, mod = divmod(self.minutes * s, 60)
|
||||||
|
self.minutes = mod * s
|
||||||
|
self.hours += div * s
|
||||||
|
if abs(self.hours) > 23:
|
||||||
|
s = _sign(self.hours)
|
||||||
|
div, mod = divmod(self.hours * s, 24)
|
||||||
|
self.hours = mod * s
|
||||||
|
self.days += div * s
|
||||||
|
if abs(self.months) > 11:
|
||||||
|
s = _sign(self.months)
|
||||||
|
div, mod = divmod(self.months * s, 12)
|
||||||
|
self.months = mod * s
|
||||||
|
self.years += div * s
|
||||||
|
if (self.hours or self.minutes or self.seconds or self.microseconds
|
||||||
|
or self.hour is not None or self.minute is not None or
|
||||||
|
self.second is not None or self.microsecond is not None):
|
||||||
|
self._has_time = 1
|
||||||
|
else:
|
||||||
|
self._has_time = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weeks(self):
|
||||||
|
return int(self.days / 7.0)
|
||||||
|
|
||||||
|
@weeks.setter
|
||||||
|
def weeks(self, value):
|
||||||
|
self.days = self.days - (self.weeks * 7) + value * 7
|
||||||
|
|
||||||
|
def _set_months(self, months):
|
||||||
|
self.months = months
|
||||||
|
if abs(self.months) > 11:
|
||||||
|
s = _sign(self.months)
|
||||||
|
div, mod = divmod(self.months * s, 12)
|
||||||
|
self.months = mod * s
|
||||||
|
self.years = div * s
|
||||||
|
else:
|
||||||
|
self.years = 0
|
||||||
|
|
||||||
|
def normalized(self):
|
||||||
|
"""
|
||||||
|
Return a version of this object represented entirely using integer
|
||||||
|
values for the relative attributes.
|
||||||
|
|
||||||
|
>>> relativedelta(days=1.5, hours=2).normalized()
|
||||||
|
relativedelta(days=+1, hours=+14)
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
||||||
|
"""
|
||||||
|
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
||||||
|
days = int(self.days)
|
||||||
|
|
||||||
|
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
||||||
|
hours = int(hours_f)
|
||||||
|
|
||||||
|
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
||||||
|
minutes = int(minutes_f)
|
||||||
|
|
||||||
|
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
||||||
|
seconds = int(seconds_f)
|
||||||
|
|
||||||
|
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
||||||
|
|
||||||
|
# Constructor carries overflow back up with call to _fix()
|
||||||
|
return self.__class__(years=self.years, months=self.months,
|
||||||
|
days=days, hours=hours, minutes=minutes,
|
||||||
|
seconds=seconds, microseconds=microseconds,
|
||||||
|
leapdays=self.leapdays, year=self.year,
|
||||||
|
month=self.month, day=self.day,
|
||||||
|
weekday=self.weekday, hour=self.hour,
|
||||||
|
minute=self.minute, second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if isinstance(other, relativedelta):
|
||||||
|
return self.__class__(years=other.years + self.years,
|
||||||
|
months=other.months + self.months,
|
||||||
|
days=other.days + self.days,
|
||||||
|
hours=other.hours + self.hours,
|
||||||
|
minutes=other.minutes + self.minutes,
|
||||||
|
seconds=other.seconds + self.seconds,
|
||||||
|
microseconds=(other.microseconds +
|
||||||
|
self.microseconds),
|
||||||
|
leapdays=other.leapdays or self.leapdays,
|
||||||
|
year=(other.year if other.year is not None
|
||||||
|
else self.year),
|
||||||
|
month=(other.month if other.month is not None
|
||||||
|
else self.month),
|
||||||
|
day=(other.day if other.day is not None
|
||||||
|
else self.day),
|
||||||
|
weekday=(other.weekday if other.weekday is not None
|
||||||
|
else self.weekday),
|
||||||
|
hour=(other.hour if other.hour is not None
|
||||||
|
else self.hour),
|
||||||
|
minute=(other.minute if other.minute is not None
|
||||||
|
else self.minute),
|
||||||
|
second=(other.second if other.second is not None
|
||||||
|
else self.second),
|
||||||
|
microsecond=(other.microsecond if other.microsecond
|
||||||
|
is not None else
|
||||||
|
self.microsecond))
|
||||||
|
if isinstance(other, datetime.timedelta):
|
||||||
|
return self.__class__(years=self.years,
|
||||||
|
months=self.months,
|
||||||
|
days=self.days + other.days,
|
||||||
|
hours=self.hours,
|
||||||
|
minutes=self.minutes,
|
||||||
|
seconds=self.seconds + other.seconds,
|
||||||
|
microseconds=self.microseconds + other.microseconds,
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
if not isinstance(other, datetime.date):
|
||||||
|
return NotImplemented
|
||||||
|
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||||
|
other = datetime.datetime.fromordinal(other.toordinal())
|
||||||
|
year = (self.year or other.year)+self.years
|
||||||
|
month = self.month or other.month
|
||||||
|
if self.months:
|
||||||
|
assert 1 <= abs(self.months) <= 12
|
||||||
|
month += self.months
|
||||||
|
if month > 12:
|
||||||
|
year += 1
|
||||||
|
month -= 12
|
||||||
|
elif month < 1:
|
||||||
|
year -= 1
|
||||||
|
month += 12
|
||||||
|
day = min(calendar.monthrange(year, month)[1],
|
||||||
|
self.day or other.day)
|
||||||
|
repl = {"year": year, "month": month, "day": day}
|
||||||
|
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value is not None:
|
||||||
|
repl[attr] = value
|
||||||
|
days = self.days
|
||||||
|
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||||
|
days += self.leapdays
|
||||||
|
ret = (other.replace(**repl)
|
||||||
|
+ datetime.timedelta(days=days,
|
||||||
|
hours=self.hours,
|
||||||
|
minutes=self.minutes,
|
||||||
|
seconds=self.seconds,
|
||||||
|
microseconds=self.microseconds))
|
||||||
|
if self.weekday:
|
||||||
|
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||||
|
jumpdays = (abs(nth) - 1) * 7
|
||||||
|
if nth > 0:
|
||||||
|
jumpdays += (7 - ret.weekday() + weekday) % 7
|
||||||
|
else:
|
||||||
|
jumpdays += (ret.weekday() - weekday) % 7
|
||||||
|
jumpdays *= -1
|
||||||
|
ret += datetime.timedelta(days=jumpdays)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self.__add__(other)
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
return self.__neg__().__radd__(other)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
if not isinstance(other, relativedelta):
|
||||||
|
return NotImplemented # In case the other object defines __rsub__
|
||||||
|
return self.__class__(years=self.years - other.years,
|
||||||
|
months=self.months - other.months,
|
||||||
|
days=self.days - other.days,
|
||||||
|
hours=self.hours - other.hours,
|
||||||
|
minutes=self.minutes - other.minutes,
|
||||||
|
seconds=self.seconds - other.seconds,
|
||||||
|
microseconds=self.microseconds - other.microseconds,
|
||||||
|
leapdays=self.leapdays or other.leapdays,
|
||||||
|
year=(self.year if self.year is not None
|
||||||
|
else other.year),
|
||||||
|
month=(self.month if self.month is not None else
|
||||||
|
other.month),
|
||||||
|
day=(self.day if self.day is not None else
|
||||||
|
other.day),
|
||||||
|
weekday=(self.weekday if self.weekday is not None else
|
||||||
|
other.weekday),
|
||||||
|
hour=(self.hour if self.hour is not None else
|
||||||
|
other.hour),
|
||||||
|
minute=(self.minute if self.minute is not None else
|
||||||
|
other.minute),
|
||||||
|
second=(self.second if self.second is not None else
|
||||||
|
other.second),
|
||||||
|
microsecond=(self.microsecond if self.microsecond
|
||||||
|
is not None else
|
||||||
|
other.microsecond))
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return self.__class__(years=abs(self.years),
|
||||||
|
months=abs(self.months),
|
||||||
|
days=abs(self.days),
|
||||||
|
hours=abs(self.hours),
|
||||||
|
minutes=abs(self.minutes),
|
||||||
|
seconds=abs(self.seconds),
|
||||||
|
microseconds=abs(self.microseconds),
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return self.__class__(years=-self.years,
|
||||||
|
months=-self.months,
|
||||||
|
days=-self.days,
|
||||||
|
hours=-self.hours,
|
||||||
|
minutes=-self.minutes,
|
||||||
|
seconds=-self.seconds,
|
||||||
|
microseconds=-self.microseconds,
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return not (not self.years and
|
||||||
|
not self.months and
|
||||||
|
not self.days and
|
||||||
|
not self.hours and
|
||||||
|
not self.minutes and
|
||||||
|
not self.seconds and
|
||||||
|
not self.microseconds and
|
||||||
|
not self.leapdays and
|
||||||
|
self.year is None and
|
||||||
|
self.month is None and
|
||||||
|
self.day is None and
|
||||||
|
self.weekday is None and
|
||||||
|
self.hour is None and
|
||||||
|
self.minute is None and
|
||||||
|
self.second is None and
|
||||||
|
self.microsecond is None)
|
||||||
|
# Compatibility with Python 2.x
|
||||||
|
__nonzero__ = __bool__
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
try:
|
||||||
|
f = float(other)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return self.__class__(years=int(self.years * f),
|
||||||
|
months=int(self.months * f),
|
||||||
|
days=int(self.days * f),
|
||||||
|
hours=int(self.hours * f),
|
||||||
|
minutes=int(self.minutes * f),
|
||||||
|
seconds=int(self.seconds * f),
|
||||||
|
microseconds=int(self.microseconds * f),
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
__rmul__ = __mul__
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, relativedelta):
|
||||||
|
return NotImplemented
|
||||||
|
if self.weekday or other.weekday:
|
||||||
|
if not self.weekday or not other.weekday:
|
||||||
|
return False
|
||||||
|
if self.weekday.weekday != other.weekday.weekday:
|
||||||
|
return False
|
||||||
|
n1, n2 = self.weekday.n, other.weekday.n
|
||||||
|
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||||
|
return False
|
||||||
|
return (self.years == other.years and
|
||||||
|
self.months == other.months and
|
||||||
|
self.days == other.days and
|
||||||
|
self.hours == other.hours and
|
||||||
|
self.minutes == other.minutes and
|
||||||
|
self.seconds == other.seconds and
|
||||||
|
self.microseconds == other.microseconds and
|
||||||
|
self.leapdays == other.leapdays and
|
||||||
|
self.year == other.year and
|
||||||
|
self.month == other.month and
|
||||||
|
self.day == other.day and
|
||||||
|
self.hour == other.hour and
|
||||||
|
self.minute == other.minute and
|
||||||
|
self.second == other.second and
|
||||||
|
self.microsecond == other.microsecond)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((
|
||||||
|
self.weekday,
|
||||||
|
self.years,
|
||||||
|
self.months,
|
||||||
|
self.days,
|
||||||
|
self.hours,
|
||||||
|
self.minutes,
|
||||||
|
self.seconds,
|
||||||
|
self.microseconds,
|
||||||
|
self.leapdays,
|
||||||
|
self.year,
|
||||||
|
self.month,
|
||||||
|
self.day,
|
||||||
|
self.hour,
|
||||||
|
self.minute,
|
||||||
|
self.second,
|
||||||
|
self.microsecond,
|
||||||
|
))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __div__(self, other):
|
||||||
|
try:
|
||||||
|
reciprocal = 1 / float(other)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return self.__mul__(reciprocal)
|
||||||
|
|
||||||
|
__truediv__ = __div__
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
l = []
|
||||||
|
for attr in ["years", "months", "days", "leapdays",
|
||||||
|
"hours", "minutes", "seconds", "microseconds"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value:
|
||||||
|
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
||||||
|
for attr in ["year", "month", "day", "weekday",
|
||||||
|
"hour", "minute", "second", "microsecond"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value is not None:
|
||||||
|
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
||||||
|
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
||||||
|
attrs=", ".join(l))
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(x):
|
||||||
|
return int(copysign(1, x))
|
||||||
|
|
||||||
|
# vim:ts=4:sw=4:et
|
||||||
1737
venv/lib/python3.12/site-packages/dateutil/rrule.py
Normal file
1737
venv/lib/python3.12/site-packages/dateutil/rrule.py
Normal file
File diff suppressed because it is too large
Load Diff
12
venv/lib/python3.12/site-packages/dateutil/tz/__init__.py
Normal file
12
venv/lib/python3.12/site-packages/dateutil/tz/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .tz import *
|
||||||
|
from .tz import __doc__
|
||||||
|
|
||||||
|
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||||
|
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||||
|
"enfold", "datetime_ambiguous", "datetime_exists",
|
||||||
|
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedTzFormatWarning(Warning):
|
||||||
|
"""Warning raised when time zones are parsed from deprecated formats."""
|
||||||
419
venv/lib/python3.12/site-packages/dateutil/tz/_common.py
Normal file
419
venv/lib/python3.12/site-packages/dateutil/tz/_common.py
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
from six import PY2
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, tzinfo
|
||||||
|
|
||||||
|
|
||||||
|
ZERO = timedelta(0)
|
||||||
|
|
||||||
|
__all__ = ['tzname_in_python2', 'enfold']
|
||||||
|
|
||||||
|
|
||||||
|
def tzname_in_python2(namefunc):
|
||||||
|
"""Change unicode output into bytestrings in Python 2
|
||||||
|
|
||||||
|
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||||
|
to unicode strings
|
||||||
|
"""
|
||||||
|
if PY2:
|
||||||
|
@wraps(namefunc)
|
||||||
|
def adjust_encoding(*args, **kwargs):
|
||||||
|
name = namefunc(*args, **kwargs)
|
||||||
|
if name is not None:
|
||||||
|
name = name.encode()
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
return adjust_encoding
|
||||||
|
else:
|
||||||
|
return namefunc
|
||||||
|
|
||||||
|
|
||||||
|
# The following is adapted from Alexander Belopolsky's tz library
|
||||||
|
# https://github.com/abalkin/tz
|
||||||
|
if hasattr(datetime, 'fold'):
|
||||||
|
# This is the pre-python 3.6 fold situation
|
||||||
|
def enfold(dt, fold=1):
|
||||||
|
"""
|
||||||
|
Provides a unified interface for assigning the ``fold`` attribute to
|
||||||
|
datetimes both before and after the implementation of PEP-495.
|
||||||
|
|
||||||
|
:param fold:
|
||||||
|
The value for the ``fold`` attribute in the returned datetime. This
|
||||||
|
should be either 0 or 1.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||||
|
``fold`` for all versions of Python. In versions prior to
|
||||||
|
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||||
|
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||||
|
attribute added, if ``fold`` is 1.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
return dt.replace(fold=fold)
|
||||||
|
|
||||||
|
else:
|
||||||
|
class _DatetimeWithFold(datetime):
|
||||||
|
"""
|
||||||
|
This is a class designed to provide a PEP 495-compliant interface for
|
||||||
|
Python versions before 3.6. It is used only for dates in a fold, so
|
||||||
|
the ``fold`` attribute is fixed at ``1``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def replace(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a datetime with the same attributes, except for those
|
||||||
|
attributes given new values by whichever keyword arguments are
|
||||||
|
specified. Note that tzinfo=None can be specified to create a naive
|
||||||
|
datetime from an aware datetime with no conversion of date and time
|
||||||
|
data.
|
||||||
|
|
||||||
|
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
||||||
|
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
||||||
|
"""
|
||||||
|
argnames = (
|
||||||
|
'year', 'month', 'day', 'hour', 'minute', 'second',
|
||||||
|
'microsecond', 'tzinfo'
|
||||||
|
)
|
||||||
|
|
||||||
|
for arg, argname in zip(args, argnames):
|
||||||
|
if argname in kwargs:
|
||||||
|
raise TypeError('Duplicate argument: {}'.format(argname))
|
||||||
|
|
||||||
|
kwargs[argname] = arg
|
||||||
|
|
||||||
|
for argname in argnames:
|
||||||
|
if argname not in kwargs:
|
||||||
|
kwargs[argname] = getattr(self, argname)
|
||||||
|
|
||||||
|
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
||||||
|
|
||||||
|
return dt_class(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fold(self):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def enfold(dt, fold=1):
|
||||||
|
"""
|
||||||
|
Provides a unified interface for assigning the ``fold`` attribute to
|
||||||
|
datetimes both before and after the implementation of PEP-495.
|
||||||
|
|
||||||
|
:param fold:
|
||||||
|
The value for the ``fold`` attribute in the returned datetime. This
|
||||||
|
should be either 0 or 1.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||||
|
``fold`` for all versions of Python. In versions prior to
|
||||||
|
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||||
|
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||||
|
attribute added, if ``fold`` is 1.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
if getattr(dt, 'fold', 0) == fold:
|
||||||
|
return dt
|
||||||
|
|
||||||
|
args = dt.timetuple()[:6]
|
||||||
|
args += (dt.microsecond, dt.tzinfo)
|
||||||
|
|
||||||
|
if fold:
|
||||||
|
return _DatetimeWithFold(*args)
|
||||||
|
else:
|
||||||
|
return datetime(*args)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_fromutc_inputs(f):
|
||||||
|
"""
|
||||||
|
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
||||||
|
object and that ``self`` is attached as its ``tzinfo``.
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def fromutc(self, dt):
|
||||||
|
if not isinstance(dt, datetime):
|
||||||
|
raise TypeError("fromutc() requires a datetime argument")
|
||||||
|
if dt.tzinfo is not self:
|
||||||
|
raise ValueError("dt.tzinfo is not self")
|
||||||
|
|
||||||
|
return f(self, dt)
|
||||||
|
|
||||||
|
return fromutc
|
||||||
|
|
||||||
|
|
||||||
|
class _tzinfo(tzinfo):
|
||||||
|
"""
|
||||||
|
Base class for all ``dateutil`` ``tzinfo`` objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_ambiguous(self, dt):
|
||||||
|
"""
|
||||||
|
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||||
|
zone.
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||||
|
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=self)
|
||||||
|
|
||||||
|
wall_0 = enfold(dt, fold=0)
|
||||||
|
wall_1 = enfold(dt, fold=1)
|
||||||
|
|
||||||
|
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
||||||
|
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
||||||
|
|
||||||
|
return same_dt and not same_offset
|
||||||
|
|
||||||
|
def _fold_status(self, dt_utc, dt_wall):
|
||||||
|
"""
|
||||||
|
Determine the fold status of a "wall" datetime, given a representation
|
||||||
|
of the same datetime as a (naive) UTC datetime. This is calculated based
|
||||||
|
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
||||||
|
datetimes, and that this offset is the actual number of hours separating
|
||||||
|
``dt_utc`` and ``dt_wall``.
|
||||||
|
|
||||||
|
:param dt_utc:
|
||||||
|
Representation of the datetime as UTC
|
||||||
|
|
||||||
|
:param dt_wall:
|
||||||
|
Representation of the datetime as "wall time". This parameter must
|
||||||
|
either have a `fold` attribute or have a fold-naive
|
||||||
|
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
||||||
|
fail.
|
||||||
|
"""
|
||||||
|
if self.is_ambiguous(dt_wall):
|
||||||
|
delta_wall = dt_wall - dt_utc
|
||||||
|
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
||||||
|
else:
|
||||||
|
_fold = 0
|
||||||
|
|
||||||
|
return _fold
|
||||||
|
|
||||||
|
def _fold(self, dt):
|
||||||
|
return getattr(dt, 'fold', 0)
|
||||||
|
|
||||||
|
def _fromutc(self, dt):
|
||||||
|
"""
|
||||||
|
Given a timezone-aware datetime in a given timezone, calculates a
|
||||||
|
timezone-aware datetime in a new timezone.
|
||||||
|
|
||||||
|
Since this is the one time that we *know* we have an unambiguous
|
||||||
|
datetime object, we take this opportunity to determine whether the
|
||||||
|
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||||
|
occurrence, chronologically, of the ambiguous datetime).
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A timezone-aware :class:`datetime.datetime` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Re-implement the algorithm from Python's datetime.py
|
||||||
|
dtoff = dt.utcoffset()
|
||||||
|
if dtoff is None:
|
||||||
|
raise ValueError("fromutc() requires a non-None utcoffset() "
|
||||||
|
"result")
|
||||||
|
|
||||||
|
# The original datetime.py code assumes that `dst()` defaults to
|
||||||
|
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
||||||
|
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
||||||
|
dtdst = dt.dst()
|
||||||
|
if dtdst is None:
|
||||||
|
raise ValueError("fromutc() requires a non-None dst() result")
|
||||||
|
delta = dtoff - dtdst
|
||||||
|
|
||||||
|
dt += delta
|
||||||
|
# Set fold=1 so we can default to being in the fold for
|
||||||
|
# ambiguous dates.
|
||||||
|
dtdst = enfold(dt, fold=1).dst()
|
||||||
|
if dtdst is None:
|
||||||
|
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
||||||
|
"results; cannot convert")
|
||||||
|
return dt + dtdst
|
||||||
|
|
||||||
|
@_validate_fromutc_inputs
|
||||||
|
def fromutc(self, dt):
|
||||||
|
"""
|
||||||
|
Given a timezone-aware datetime in a given timezone, calculates a
|
||||||
|
timezone-aware datetime in a new timezone.
|
||||||
|
|
||||||
|
Since this is the one time that we *know* we have an unambiguous
|
||||||
|
datetime object, we take this opportunity to determine whether the
|
||||||
|
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||||
|
occurrence, chronologically, of the ambiguous datetime).
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A timezone-aware :class:`datetime.datetime` object.
|
||||||
|
"""
|
||||||
|
dt_wall = self._fromutc(dt)
|
||||||
|
|
||||||
|
# Calculate the fold status given the two datetimes.
|
||||||
|
_fold = self._fold_status(dt, dt_wall)
|
||||||
|
|
||||||
|
# Set the default fold value for ambiguous dates
|
||||||
|
return enfold(dt_wall, fold=_fold)
|
||||||
|
|
||||||
|
|
||||||
|
class tzrangebase(_tzinfo):
|
||||||
|
"""
|
||||||
|
This is an abstract base class for time zones represented by an annual
|
||||||
|
transition into and out of DST. Child classes should implement the following
|
||||||
|
methods:
|
||||||
|
|
||||||
|
* ``__init__(self, *args, **kwargs)``
|
||||||
|
* ``transitions(self, year)`` - this is expected to return a tuple of
|
||||||
|
datetimes representing the DST on and off transitions in standard
|
||||||
|
time.
|
||||||
|
|
||||||
|
A fully initialized ``tzrangebase`` subclass should also provide the
|
||||||
|
following attributes:
|
||||||
|
* ``hasdst``: Boolean whether or not the zone uses DST.
|
||||||
|
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
||||||
|
representing the respective UTC offsets.
|
||||||
|
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
||||||
|
abbreviations in DST and STD, respectively.
|
||||||
|
* ``_hasdst``: Whether or not the zone has DST.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError('tzrangebase is an abstract base class')
|
||||||
|
|
||||||
|
def utcoffset(self, dt):
|
||||||
|
isdst = self._isdst(dt)
|
||||||
|
|
||||||
|
if isdst is None:
|
||||||
|
return None
|
||||||
|
elif isdst:
|
||||||
|
return self._dst_offset
|
||||||
|
else:
|
||||||
|
return self._std_offset
|
||||||
|
|
||||||
|
def dst(self, dt):
|
||||||
|
isdst = self._isdst(dt)
|
||||||
|
|
||||||
|
if isdst is None:
|
||||||
|
return None
|
||||||
|
elif isdst:
|
||||||
|
return self._dst_base_offset
|
||||||
|
else:
|
||||||
|
return ZERO
|
||||||
|
|
||||||
|
@tzname_in_python2
|
||||||
|
def tzname(self, dt):
|
||||||
|
if self._isdst(dt):
|
||||||
|
return self._dst_abbr
|
||||||
|
else:
|
||||||
|
return self._std_abbr
|
||||||
|
|
||||||
|
def fromutc(self, dt):
|
||||||
|
""" Given a datetime in UTC, return local time """
|
||||||
|
if not isinstance(dt, datetime):
|
||||||
|
raise TypeError("fromutc() requires a datetime argument")
|
||||||
|
|
||||||
|
if dt.tzinfo is not self:
|
||||||
|
raise ValueError("dt.tzinfo is not self")
|
||||||
|
|
||||||
|
# Get transitions - if there are none, fixed offset
|
||||||
|
transitions = self.transitions(dt.year)
|
||||||
|
if transitions is None:
|
||||||
|
return dt + self.utcoffset(dt)
|
||||||
|
|
||||||
|
# Get the transition times in UTC
|
||||||
|
dston, dstoff = transitions
|
||||||
|
|
||||||
|
dston -= self._std_offset
|
||||||
|
dstoff -= self._std_offset
|
||||||
|
|
||||||
|
utc_transitions = (dston, dstoff)
|
||||||
|
dt_utc = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
||||||
|
|
||||||
|
if isdst:
|
||||||
|
dt_wall = dt + self._dst_offset
|
||||||
|
else:
|
||||||
|
dt_wall = dt + self._std_offset
|
||||||
|
|
||||||
|
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
||||||
|
|
||||||
|
return enfold(dt_wall, fold=_fold)
|
||||||
|
|
||||||
|
def is_ambiguous(self, dt):
|
||||||
|
"""
|
||||||
|
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||||
|
zone.
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||||
|
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
if not self.hasdst:
|
||||||
|
return False
|
||||||
|
|
||||||
|
start, end = self.transitions(dt.year)
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
return (end <= dt < end + self._dst_base_offset)
|
||||||
|
|
||||||
|
def _isdst(self, dt):
|
||||||
|
if not self.hasdst:
|
||||||
|
return False
|
||||||
|
elif dt is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
transitions = self.transitions(dt.year)
|
||||||
|
|
||||||
|
if transitions is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
isdst = self._naive_isdst(dt, transitions)
|
||||||
|
|
||||||
|
# Handle ambiguous dates
|
||||||
|
if not isdst and self.is_ambiguous(dt):
|
||||||
|
return not self._fold(dt)
|
||||||
|
else:
|
||||||
|
return isdst
|
||||||
|
|
||||||
|
def _naive_isdst(self, dt, transitions):
|
||||||
|
dston, dstoff = transitions
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
if dston < dstoff:
|
||||||
|
isdst = dston <= dt < dstoff
|
||||||
|
else:
|
||||||
|
isdst = not dstoff <= dt < dston
|
||||||
|
|
||||||
|
return isdst
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dst_base_offset(self):
|
||||||
|
return self._dst_offset - self._std_offset
|
||||||
|
|
||||||
|
__hash__ = None
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s(...)" % self.__class__.__name__
|
||||||
|
|
||||||
|
__reduce__ = object.__reduce__
|
||||||
80
venv/lib/python3.12/site-packages/dateutil/tz/_factories.py
Normal file
80
venv/lib/python3.12/site-packages/dateutil/tz/_factories.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
import weakref
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from six.moves import _thread
|
||||||
|
|
||||||
|
|
||||||
|
class _TzSingleton(type):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instance = None
|
||||||
|
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __call__(cls):
|
||||||
|
if cls.__instance is None:
|
||||||
|
cls.__instance = super(_TzSingleton, cls).__call__()
|
||||||
|
return cls.__instance
|
||||||
|
|
||||||
|
|
||||||
|
class _TzFactory(type):
|
||||||
|
def instance(cls, *args, **kwargs):
|
||||||
|
"""Alternate constructor that returns a fresh instance"""
|
||||||
|
return type.__call__(cls, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class _TzOffsetFactory(_TzFactory):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instances = weakref.WeakValueDictionary()
|
||||||
|
cls.__strong_cache = OrderedDict()
|
||||||
|
cls.__strong_cache_size = 8
|
||||||
|
|
||||||
|
cls._cache_lock = _thread.allocate_lock()
|
||||||
|
|
||||||
|
def __call__(cls, name, offset):
|
||||||
|
if isinstance(offset, timedelta):
|
||||||
|
key = (name, offset.total_seconds())
|
||||||
|
else:
|
||||||
|
key = (name, offset)
|
||||||
|
|
||||||
|
instance = cls.__instances.get(key, None)
|
||||||
|
if instance is None:
|
||||||
|
instance = cls.__instances.setdefault(key,
|
||||||
|
cls.instance(name, offset))
|
||||||
|
|
||||||
|
# This lock may not be necessary in Python 3. See GH issue #901
|
||||||
|
with cls._cache_lock:
|
||||||
|
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||||
|
|
||||||
|
# Remove an item if the strong cache is overpopulated
|
||||||
|
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||||
|
cls.__strong_cache.popitem(last=False)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class _TzStrFactory(_TzFactory):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instances = weakref.WeakValueDictionary()
|
||||||
|
cls.__strong_cache = OrderedDict()
|
||||||
|
cls.__strong_cache_size = 8
|
||||||
|
|
||||||
|
cls.__cache_lock = _thread.allocate_lock()
|
||||||
|
|
||||||
|
def __call__(cls, s, posix_offset=False):
|
||||||
|
key = (s, posix_offset)
|
||||||
|
instance = cls.__instances.get(key, None)
|
||||||
|
|
||||||
|
if instance is None:
|
||||||
|
instance = cls.__instances.setdefault(key,
|
||||||
|
cls.instance(s, posix_offset))
|
||||||
|
|
||||||
|
# This lock may not be necessary in Python 3. See GH issue #901
|
||||||
|
with cls.__cache_lock:
|
||||||
|
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||||
|
|
||||||
|
# Remove an item if the strong cache is overpopulated
|
||||||
|
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||||
|
cls.__strong_cache.popitem(last=False)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
1849
venv/lib/python3.12/site-packages/dateutil/tz/tz.py
Normal file
1849
venv/lib/python3.12/site-packages/dateutil/tz/tz.py
Normal file
File diff suppressed because it is too large
Load Diff
370
venv/lib/python3.12/site-packages/dateutil/tz/win.py
Normal file
370
venv/lib/python3.12/site-packages/dateutil/tz/win.py
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module provides an interface to the native time zone data on Windows,
|
||||||
|
including :py:class:`datetime.tzinfo` implementations.
|
||||||
|
|
||||||
|
Attempting to import this module on a non-Windows platform will raise an
|
||||||
|
:py:obj:`ImportError`.
|
||||||
|
"""
|
||||||
|
# This code was originally contributed by Jeffrey Harris.
|
||||||
|
import datetime
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from six.moves import winreg
|
||||||
|
from six import text_type
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
except ValueError:
|
||||||
|
# ValueError is raised on non-Windows systems for some horrible reason.
|
||||||
|
raise ImportError("Running tzwin on non-Windows system")
|
||||||
|
|
||||||
|
from ._common import tzrangebase
|
||||||
|
|
||||||
|
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
||||||
|
|
||||||
|
ONEWEEK = datetime.timedelta(7)
|
||||||
|
|
||||||
|
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||||
|
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||||
|
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||||
|
|
||||||
|
|
||||||
|
def _settzkeyname():
|
||||||
|
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||||
|
try:
|
||||||
|
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||||
|
TZKEYNAME = TZKEYNAMENT
|
||||||
|
except WindowsError:
|
||||||
|
TZKEYNAME = TZKEYNAME9X
|
||||||
|
handle.Close()
|
||||||
|
return TZKEYNAME
|
||||||
|
|
||||||
|
|
||||||
|
TZKEYNAME = _settzkeyname()
|
||||||
|
|
||||||
|
|
||||||
|
class tzres(object):
|
||||||
|
"""
|
||||||
|
Class for accessing ``tzres.dll``, which contains timezone name related
|
||||||
|
resources.
|
||||||
|
|
||||||
|
.. versionadded:: 2.5.0
|
||||||
|
"""
|
||||||
|
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
||||||
|
|
||||||
|
def __init__(self, tzres_loc='tzres.dll'):
|
||||||
|
# Load the user32 DLL so we can load strings from tzres
|
||||||
|
user32 = ctypes.WinDLL('user32')
|
||||||
|
|
||||||
|
# Specify the LoadStringW function
|
||||||
|
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.LPWSTR,
|
||||||
|
ctypes.c_int)
|
||||||
|
|
||||||
|
self.LoadStringW = user32.LoadStringW
|
||||||
|
self._tzres = ctypes.WinDLL(tzres_loc)
|
||||||
|
self.tzres_loc = tzres_loc
|
||||||
|
|
||||||
|
def load_name(self, offset):
|
||||||
|
"""
|
||||||
|
Load a timezone name from a DLL offset (integer).
|
||||||
|
|
||||||
|
>>> from dateutil.tzwin import tzres
|
||||||
|
>>> tzr = tzres()
|
||||||
|
>>> print(tzr.load_name(112))
|
||||||
|
'Eastern Standard Time'
|
||||||
|
|
||||||
|
:param offset:
|
||||||
|
A positive integer value referring to a string from the tzres dll.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Offsets found in the registry are generally of the form
|
||||||
|
``@tzres.dll,-114``. The offset in this case is 114, not -114.
|
||||||
|
|
||||||
|
"""
|
||||||
|
resource = self.p_wchar()
|
||||||
|
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
||||||
|
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
||||||
|
return resource[:nchar]
|
||||||
|
|
||||||
|
def name_from_string(self, tzname_str):
|
||||||
|
"""
|
||||||
|
Parse strings as returned from the Windows registry into the time zone
|
||||||
|
name as defined in the registry.
|
||||||
|
|
||||||
|
>>> from dateutil.tzwin import tzres
|
||||||
|
>>> tzr = tzres()
|
||||||
|
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
||||||
|
'Dateline Daylight Time'
|
||||||
|
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
||||||
|
'Eastern Standard Time'
|
||||||
|
|
||||||
|
:param tzname_str:
|
||||||
|
A timezone name string as returned from a Windows registry key.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns the localized timezone string from tzres.dll if the string
|
||||||
|
is of the form `@tzres.dll,-offset`, else returns the input string.
|
||||||
|
"""
|
||||||
|
if not tzname_str.startswith('@'):
|
||||||
|
return tzname_str
|
||||||
|
|
||||||
|
name_splt = tzname_str.split(',-')
|
||||||
|
try:
|
||||||
|
offset = int(name_splt[1])
|
||||||
|
except:
|
||||||
|
raise ValueError("Malformed timezone string.")
|
||||||
|
|
||||||
|
return self.load_name(offset)
|
||||||
|
|
||||||
|
|
||||||
|
class tzwinbase(tzrangebase):
|
||||||
|
"""tzinfo class based on win32's timezones available in the registry."""
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError('tzwinbase is an abstract base class')
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
# Compare on all relevant dimensions, including name.
|
||||||
|
if not isinstance(other, tzwinbase):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return (self._std_offset == other._std_offset and
|
||||||
|
self._dst_offset == other._dst_offset and
|
||||||
|
self._stddayofweek == other._stddayofweek and
|
||||||
|
self._dstdayofweek == other._dstdayofweek and
|
||||||
|
self._stdweeknumber == other._stdweeknumber and
|
||||||
|
self._dstweeknumber == other._dstweeknumber and
|
||||||
|
self._stdhour == other._stdhour and
|
||||||
|
self._dsthour == other._dsthour and
|
||||||
|
self._stdminute == other._stdminute and
|
||||||
|
self._dstminute == other._dstminute and
|
||||||
|
self._std_abbr == other._std_abbr and
|
||||||
|
self._dst_abbr == other._dst_abbr)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list():
|
||||||
|
"""Return a list of all time zones known to the system."""
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
||||||
|
result = [winreg.EnumKey(tzkey, i)
|
||||||
|
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
"""
|
||||||
|
Return the display name of the time zone.
|
||||||
|
"""
|
||||||
|
return self._display
|
||||||
|
|
||||||
|
def transitions(self, year):
|
||||||
|
"""
|
||||||
|
For a given year, get the DST on and off transition times, expressed
|
||||||
|
always on the standard time side. For zones with no transitions, this
|
||||||
|
function returns ``None``.
|
||||||
|
|
||||||
|
:param year:
|
||||||
|
The year whose transitions you would like to query.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
||||||
|
``(dston, dstoff)`` for zones with an annual DST transition, or
|
||||||
|
``None`` for fixed offset zones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.hasdst:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
||||||
|
self._dsthour, self._dstminute,
|
||||||
|
self._dstweeknumber)
|
||||||
|
|
||||||
|
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
||||||
|
self._stdhour, self._stdminute,
|
||||||
|
self._stdweeknumber)
|
||||||
|
|
||||||
|
# Ambiguous dates default to the STD side
|
||||||
|
dstoff -= self._dst_base_offset
|
||||||
|
|
||||||
|
return dston, dstoff
|
||||||
|
|
||||||
|
def _get_hasdst(self):
|
||||||
|
return self._dstmonth != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dst_base_offset(self):
|
||||||
|
return self._dst_base_offset_
|
||||||
|
|
||||||
|
|
||||||
|
class tzwin(tzwinbase):
|
||||||
|
"""
|
||||||
|
Time zone object created from the zone info in the Windows registry
|
||||||
|
|
||||||
|
These are similar to :py:class:`dateutil.tz.tzrange` objects in that
|
||||||
|
the time zone data is provided in the format of a single offset rule
|
||||||
|
for either 0 or 2 time zone transitions per year.
|
||||||
|
|
||||||
|
:param: name
|
||||||
|
The name of a Windows time zone key, e.g. "Eastern Standard Time".
|
||||||
|
The full list of keys can be retrieved with :func:`tzwin.list`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
||||||
|
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||||
|
keydict = valuestodict(tzkey)
|
||||||
|
|
||||||
|
self._std_abbr = keydict["Std"]
|
||||||
|
self._dst_abbr = keydict["Dlt"]
|
||||||
|
|
||||||
|
self._display = keydict["Display"]
|
||||||
|
|
||||||
|
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||||
|
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||||
|
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||||
|
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
||||||
|
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||||
|
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||||
|
|
||||||
|
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||||
|
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||||
|
(self._stdmonth,
|
||||||
|
self._stddayofweek, # Sunday = 0
|
||||||
|
self._stdweeknumber, # Last = 5
|
||||||
|
self._stdhour,
|
||||||
|
self._stdminute) = tup[4:9]
|
||||||
|
|
||||||
|
(self._dstmonth,
|
||||||
|
self._dstdayofweek, # Sunday = 0
|
||||||
|
self._dstweeknumber, # Last = 5
|
||||||
|
self._dsthour,
|
||||||
|
self._dstminute) = tup[12:17]
|
||||||
|
|
||||||
|
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||||
|
self.hasdst = self._get_hasdst()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "tzwin(%s)" % repr(self._name)
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (self.__class__, (self._name,))
|
||||||
|
|
||||||
|
|
||||||
|
class tzwinlocal(tzwinbase):
|
||||||
|
"""
|
||||||
|
Class representing the local time zone information in the Windows registry
|
||||||
|
|
||||||
|
While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time`
|
||||||
|
module) to retrieve time zone information, ``tzwinlocal`` retrieves the
|
||||||
|
rules directly from the Windows registry and creates an object like
|
||||||
|
:class:`dateutil.tz.tzwin`.
|
||||||
|
|
||||||
|
Because Windows does not have an equivalent of :func:`time.tzset`, on
|
||||||
|
Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the
|
||||||
|
time zone settings *at the time that the process was started*, meaning
|
||||||
|
changes to the machine's time zone settings during the run of a program
|
||||||
|
on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`.
|
||||||
|
Because ``tzwinlocal`` reads the registry directly, it is unaffected by
|
||||||
|
this issue.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
||||||
|
keydict = valuestodict(tzlocalkey)
|
||||||
|
|
||||||
|
self._std_abbr = keydict["StandardName"]
|
||||||
|
self._dst_abbr = keydict["DaylightName"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
||||||
|
sn=self._std_abbr)
|
||||||
|
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||||
|
_keydict = valuestodict(tzkey)
|
||||||
|
self._display = _keydict["Display"]
|
||||||
|
except OSError:
|
||||||
|
self._display = None
|
||||||
|
|
||||||
|
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||||
|
dstoffset = stdoffset-keydict["DaylightBias"]
|
||||||
|
|
||||||
|
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||||
|
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||||
|
|
||||||
|
# For reasons unclear, in this particular key, the day of week has been
|
||||||
|
# moved to the END of the SYSTEMTIME structure.
|
||||||
|
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||||
|
|
||||||
|
(self._stdmonth,
|
||||||
|
self._stdweeknumber, # Last = 5
|
||||||
|
self._stdhour,
|
||||||
|
self._stdminute) = tup[1:5]
|
||||||
|
|
||||||
|
self._stddayofweek = tup[7]
|
||||||
|
|
||||||
|
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||||
|
|
||||||
|
(self._dstmonth,
|
||||||
|
self._dstweeknumber, # Last = 5
|
||||||
|
self._dsthour,
|
||||||
|
self._dstminute) = tup[1:5]
|
||||||
|
|
||||||
|
self._dstdayofweek = tup[7]
|
||||||
|
|
||||||
|
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||||
|
self.hasdst = self._get_hasdst()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "tzwinlocal()"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# str will return the standard name, not the daylight name.
|
||||||
|
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (self.__class__, ())
|
||||||
|
|
||||||
|
|
||||||
|
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||||
|
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
||||||
|
first = datetime.datetime(year, month, 1, hour, minute)
|
||||||
|
|
||||||
|
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
||||||
|
# Because 7 % 7 = 0
|
||||||
|
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
||||||
|
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
||||||
|
if (wd.month != month):
|
||||||
|
wd -= ONEWEEK
|
||||||
|
|
||||||
|
return wd
|
||||||
|
|
||||||
|
|
||||||
|
def valuestodict(key):
|
||||||
|
"""Convert a registry key's values to a dictionary."""
|
||||||
|
dout = {}
|
||||||
|
size = winreg.QueryInfoKey(key)[1]
|
||||||
|
tz_res = None
|
||||||
|
|
||||||
|
for i in range(size):
|
||||||
|
key_name, value, dtype = winreg.EnumValue(key, i)
|
||||||
|
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
||||||
|
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
||||||
|
# that to a proper signed integer
|
||||||
|
if value & (1 << 31):
|
||||||
|
value = value - (1 << 32)
|
||||||
|
elif dtype == winreg.REG_SZ:
|
||||||
|
# If it's a reference to the tzres DLL, load the actual string
|
||||||
|
if value.startswith('@tzres'):
|
||||||
|
tz_res = tz_res or tzres()
|
||||||
|
value = tz_res.name_from_string(value)
|
||||||
|
|
||||||
|
value = value.rstrip('\x00') # Remove trailing nulls
|
||||||
|
|
||||||
|
dout[key_name] = value
|
||||||
|
|
||||||
|
return dout
|
||||||
2
venv/lib/python3.12/site-packages/dateutil/tzwin.py
Normal file
2
venv/lib/python3.12/site-packages/dateutil/tzwin.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# tzwin has moved to dateutil.tz.win
|
||||||
|
from .tz.win import *
|
||||||
71
venv/lib/python3.12/site-packages/dateutil/utils.py
Normal file
71
venv/lib/python3.12/site-packages/dateutil/utils.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers general convenience and utility functions for dealing with
|
||||||
|
datetimes.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
|
||||||
|
def today(tzinfo=None):
|
||||||
|
"""
|
||||||
|
Returns a :py:class:`datetime` representing the current day at midnight
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The time zone to attach (also used to determine the current day).
|
||||||
|
|
||||||
|
:return:
|
||||||
|
A :py:class:`datetime.datetime` object representing the current day
|
||||||
|
at midnight.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dt = datetime.now(tzinfo)
|
||||||
|
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
||||||
|
|
||||||
|
|
||||||
|
def default_tzinfo(dt, tzinfo):
|
||||||
|
"""
|
||||||
|
Sets the ``tzinfo`` parameter on naive datetimes only
|
||||||
|
|
||||||
|
This is useful for example when you are provided a datetime that may have
|
||||||
|
either an implicit or explicit time zone, such as when parsing a time zone
|
||||||
|
string.
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> from dateutil.tz import tzoffset
|
||||||
|
>>> from dateutil.parser import parse
|
||||||
|
>>> from dateutil.utils import default_tzinfo
|
||||||
|
>>> dflt_tz = tzoffset("EST", -18000)
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00+00:00
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00-05:00
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
The datetime on which to replace the time zone
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
||||||
|
``dt`` if (and only if) it is naive.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an aware :py:class:`datetime.datetime`.
|
||||||
|
"""
|
||||||
|
if dt.tzinfo is not None:
|
||||||
|
return dt
|
||||||
|
else:
|
||||||
|
return dt.replace(tzinfo=tzinfo)
|
||||||
|
|
||||||
|
|
||||||
|
def within_delta(dt1, dt2, delta):
|
||||||
|
"""
|
||||||
|
Useful for comparing two datetimes that may have a negligible difference
|
||||||
|
to be considered equal.
|
||||||
|
"""
|
||||||
|
delta = abs(delta)
|
||||||
|
difference = dt1 - dt2
|
||||||
|
return -delta <= difference <= delta
|
||||||
167
venv/lib/python3.12/site-packages/dateutil/zoneinfo/__init__.py
Normal file
167
venv/lib/python3.12/site-packages/dateutil/zoneinfo/__init__.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import warnings
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tarfile import TarFile
|
||||||
|
from pkgutil import get_data
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from dateutil.tz import tzfile as _tzfile
|
||||||
|
|
||||||
|
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||||
|
|
||||||
|
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||||
|
METADATA_FN = 'METADATA'
|
||||||
|
|
||||||
|
|
||||||
|
class tzfile(_tzfile):
|
||||||
|
def __reduce__(self):
|
||||||
|
return (gettz, (self._filename,))
|
||||||
|
|
||||||
|
|
||||||
|
def getzoneinfofile_stream():
|
||||||
|
try:
|
||||||
|
return BytesIO(get_data(__name__, ZONEFILENAME))
|
||||||
|
except IOError as e: # TODO switch to FileNotFoundError?
|
||||||
|
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ZoneInfoFile(object):
|
||||||
|
def __init__(self, zonefile_stream=None):
|
||||||
|
if zonefile_stream is not None:
|
||||||
|
with TarFile.open(fileobj=zonefile_stream) as tf:
|
||||||
|
self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
||||||
|
for zf in tf.getmembers()
|
||||||
|
if zf.isfile() and zf.name != METADATA_FN}
|
||||||
|
# deal with links: They'll point to their parent object. Less
|
||||||
|
# waste of memory
|
||||||
|
links = {zl.name: self.zones[zl.linkname]
|
||||||
|
for zl in tf.getmembers() if
|
||||||
|
zl.islnk() or zl.issym()}
|
||||||
|
self.zones.update(links)
|
||||||
|
try:
|
||||||
|
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
||||||
|
metadata_str = metadata_json.read().decode('UTF-8')
|
||||||
|
self.metadata = json.loads(metadata_str)
|
||||||
|
except KeyError:
|
||||||
|
# no metadata in tar file
|
||||||
|
self.metadata = None
|
||||||
|
else:
|
||||||
|
self.zones = {}
|
||||||
|
self.metadata = None
|
||||||
|
|
||||||
|
def get(self, name, default=None):
|
||||||
|
"""
|
||||||
|
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
||||||
|
for retrieving zones from the zone dictionary.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
The name of the zone to retrieve. (Generally IANA zone names)
|
||||||
|
|
||||||
|
:param default:
|
||||||
|
The value to return in the event of a missing key.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.zones.get(name, default)
|
||||||
|
|
||||||
|
|
||||||
|
# The current API has gettz as a module function, although in fact it taps into
|
||||||
|
# a stateful class. So as a workaround for now, without changing the API, we
|
||||||
|
# will create a new "global" class instance the first time a user requests a
|
||||||
|
# timezone. Ugly, but adheres to the api.
|
||||||
|
#
|
||||||
|
# TODO: Remove after deprecation period.
|
||||||
|
_CLASS_ZONE_INSTANCE = []
|
||||||
|
|
||||||
|
|
||||||
|
def get_zonefile_instance(new_instance=False):
|
||||||
|
"""
|
||||||
|
This is a convenience function which provides a :class:`ZoneInfoFile`
|
||||||
|
instance using the data provided by the ``dateutil`` package. By default, it
|
||||||
|
caches a single instance of the ZoneInfoFile object and returns that.
|
||||||
|
|
||||||
|
:param new_instance:
|
||||||
|
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
||||||
|
used as the cached instance for the next call. Otherwise, new instances
|
||||||
|
are created only as necessary.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`ZoneInfoFile` object.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
if new_instance:
|
||||||
|
zif = None
|
||||||
|
else:
|
||||||
|
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
||||||
|
|
||||||
|
if zif is None:
|
||||||
|
zif = ZoneInfoFile(getzoneinfofile_stream())
|
||||||
|
|
||||||
|
get_zonefile_instance._cached_instance = zif
|
||||||
|
|
||||||
|
return zif
|
||||||
|
|
||||||
|
|
||||||
|
def gettz(name):
|
||||||
|
"""
|
||||||
|
This retrieves a time zone from the local zoneinfo tarball that is packaged
|
||||||
|
with dateutil.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
An IANA-style time zone name, as found in the zoneinfo file.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
It is generally inadvisable to use this function, and it is only
|
||||||
|
provided for API compatibility with earlier versions. This is *not*
|
||||||
|
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
||||||
|
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
||||||
|
for accessing the dateutil-specific zoneinfo (which may be out of
|
||||||
|
date compared to the system zoneinfo).
|
||||||
|
|
||||||
|
.. deprecated:: 2.6
|
||||||
|
If you need to use a specific zoneinfofile over the system zoneinfo,
|
||||||
|
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
||||||
|
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
||||||
|
|
||||||
|
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
||||||
|
dateutil-provided zoneinfo.
|
||||||
|
"""
|
||||||
|
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
||||||
|
"to use the dateutil-provided zoneinfo files, instantiate a "
|
||||||
|
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
||||||
|
"instead. See the documentation for details.",
|
||||||
|
DeprecationWarning)
|
||||||
|
|
||||||
|
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||||
|
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||||
|
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
def gettz_db_metadata():
|
||||||
|
""" Get the zonefile metadata
|
||||||
|
|
||||||
|
See `zonefile_metadata`_
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
A dictionary with the database metadata
|
||||||
|
|
||||||
|
.. deprecated:: 2.6
|
||||||
|
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
||||||
|
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
||||||
|
"""
|
||||||
|
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
||||||
|
"versions, to use the dateutil-provided zoneinfo files, "
|
||||||
|
"ZoneInfoFile object and query the 'metadata' attribute "
|
||||||
|
"instead. See the documentation for details.",
|
||||||
|
DeprecationWarning)
|
||||||
|
|
||||||
|
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||||
|
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||||
|
return _CLASS_ZONE_INSTANCE[0].metadata
|
||||||
Binary file not shown.
@@ -0,0 +1,75 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from subprocess import check_call, check_output
|
||||||
|
from tarfile import TarFile
|
||||||
|
|
||||||
|
from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
||||||
|
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
||||||
|
|
||||||
|
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
zonedir = os.path.join(tmpdir, "zoneinfo")
|
||||||
|
moduledir = os.path.dirname(__file__)
|
||||||
|
try:
|
||||||
|
with TarFile.open(filename) as tf:
|
||||||
|
for name in zonegroups:
|
||||||
|
tf.extract(name, tmpdir)
|
||||||
|
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
|
||||||
|
|
||||||
|
_run_zic(zonedir, filepaths)
|
||||||
|
|
||||||
|
# write metadata file
|
||||||
|
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=4, sort_keys=True)
|
||||||
|
target = os.path.join(moduledir, ZONEFILENAME)
|
||||||
|
with TarFile.open(target, "w:%s" % format) as tf:
|
||||||
|
for entry in os.listdir(zonedir):
|
||||||
|
entrypath = os.path.join(zonedir, entry)
|
||||||
|
tf.add(entrypath, entry)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_zic(zonedir, filepaths):
|
||||||
|
"""Calls the ``zic`` compiler in a compatible way to get a "fat" binary.
|
||||||
|
|
||||||
|
Recent versions of ``zic`` default to ``-b slim``, while older versions
|
||||||
|
don't even have the ``-b`` option (but default to "fat" binaries). The
|
||||||
|
current version of dateutil does not support Version 2+ TZif files, which
|
||||||
|
causes problems when used in conjunction with "slim" binaries, so this
|
||||||
|
function is used to ensure that we always get a "fat" binary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
help_text = check_output(["zic", "--help"])
|
||||||
|
except OSError as e:
|
||||||
|
_print_on_nosuchfile(e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if b"-b " in help_text:
|
||||||
|
bloat_args = ["-b", "fat"]
|
||||||
|
else:
|
||||||
|
bloat_args = []
|
||||||
|
|
||||||
|
check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_on_nosuchfile(e):
|
||||||
|
"""Print helpful troubleshooting message
|
||||||
|
|
||||||
|
e is an exception raised by subprocess.check_call()
|
||||||
|
|
||||||
|
"""
|
||||||
|
if e.errno == 2:
|
||||||
|
logging.error(
|
||||||
|
"Could not find zic. Perhaps you need to install "
|
||||||
|
"libc-bin or some other package that provides it, "
|
||||||
|
"or it's not in your PATH?")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
The authors in alphabetical order
|
||||||
|
|
||||||
|
* Charlie Clark
|
||||||
|
* Daniel Hillier
|
||||||
|
* Elias Rabel
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
et_xml is licensed under the MIT license; see the file LICENCE for details.
|
||||||
|
|
||||||
|
et_xml includes code from the Python standard library, which is licensed under
|
||||||
|
the Python license, a permissive open source license. The copyright and license
|
||||||
|
is included below for compliance with Python's terms.
|
||||||
|
|
||||||
|
This module includes corrections and new features as follows:
|
||||||
|
- Correct handling of attributes namespaces when a default namespace
|
||||||
|
has been registered.
|
||||||
|
- Records the namespaces for an Element during parsing and utilises them to
|
||||||
|
allow inspection of namespaces at specific elements in the xml tree and
|
||||||
|
during serialisation.
|
||||||
|
|
||||||
|
Misc:
|
||||||
|
- Includes the test_xml_etree with small modifications for testing the
|
||||||
|
modifications in this package.
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 2001-present Python Software Foundation; All Rights Reserved
|
||||||
|
|
||||||
|
A. HISTORY OF THE SOFTWARE
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||||
|
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||||
|
as a successor of a language called ABC. Guido remains Python's
|
||||||
|
principal author, although it includes many contributions from others.
|
||||||
|
|
||||||
|
In 1995, Guido continued his work on Python at the Corporation for
|
||||||
|
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||||
|
in Reston, Virginia where he released several versions of the
|
||||||
|
software.
|
||||||
|
|
||||||
|
In May 2000, Guido and the Python core development team moved to
|
||||||
|
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||||
|
year, the PythonLabs team moved to Digital Creations, which became
|
||||||
|
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||||
|
https://www.python.org/psf/) was formed, a non-profit organization
|
||||||
|
created specifically to own Python-related Intellectual Property.
|
||||||
|
Zope Corporation was a sponsoring member of the PSF.
|
||||||
|
|
||||||
|
All Python releases are Open Source (see https://opensource.org for
|
||||||
|
the Open Source Definition). Historically, most, but not all, Python
|
||||||
|
releases have also been GPL-compatible; the table below summarizes
|
||||||
|
the various releases.
|
||||||
|
|
||||||
|
Release Derived Year Owner GPL-
|
||||||
|
from compatible? (1)
|
||||||
|
|
||||||
|
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||||
|
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||||
|
1.6 1.5.2 2000 CNRI no
|
||||||
|
2.0 1.6 2000 BeOpen.com no
|
||||||
|
1.6.1 1.6 2001 CNRI yes (2)
|
||||||
|
2.1 2.0+1.6.1 2001 PSF no
|
||||||
|
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||||
|
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||||
|
2.1.2 2.1.1 2002 PSF yes
|
||||||
|
2.1.3 2.1.2 2002 PSF yes
|
||||||
|
2.2 and above 2.1.1 2001-now PSF yes
|
||||||
|
|
||||||
|
Footnotes:
|
||||||
|
|
||||||
|
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||||
|
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||||
|
a modified version without making your changes open source. The
|
||||||
|
GPL-compatible licenses make it possible to combine Python with
|
||||||
|
other software that is released under the GPL; the others don't.
|
||||||
|
|
||||||
|
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||||
|
because its license has a choice of law clause. According to
|
||||||
|
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||||
|
is "not incompatible" with the GPL.
|
||||||
|
|
||||||
|
Thanks to the many outside volunteers who have worked under Guido's
|
||||||
|
direction to make these releases possible.
|
||||||
|
|
||||||
|
|
||||||
|
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||||
|
===============================================================
|
||||||
|
|
||||||
|
Python software and documentation are licensed under the
|
||||||
|
Python Software Foundation License Version 2.
|
||||||
|
|
||||||
|
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||||
|
the documentation are dual licensed under the PSF License Version 2
|
||||||
|
and the Zero-Clause BSD license.
|
||||||
|
|
||||||
|
Some software incorporated into Python is under different licenses.
|
||||||
|
The licenses are listed with code falling under that license.
|
||||||
|
|
||||||
|
|
||||||
|
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||||
|
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||||
|
otherwise using this software ("Python") in source or binary form and
|
||||||
|
its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||||
|
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||||
|
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||||
|
distribute, and otherwise use Python alone or in any derivative version,
|
||||||
|
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||||
|
i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved"
|
||||||
|
are retained in Python alone or in any derivative version prepared by Licensee.
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python.
|
||||||
|
|
||||||
|
4. PSF is making Python available to Licensee on an "AS IS"
|
||||||
|
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. Nothing in this License Agreement shall be deemed to create any
|
||||||
|
relationship of agency, partnership, or joint venture between PSF and
|
||||||
|
Licensee. This License Agreement does not grant permission to use PSF
|
||||||
|
trademarks or trade name in a trademark sense to endorse or promote
|
||||||
|
products or services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By copying, installing or otherwise using Python, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||||
|
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||||
|
Individual or Organization ("Licensee") accessing and otherwise using
|
||||||
|
this software in source or binary form and its associated
|
||||||
|
documentation ("the Software").
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this BeOpen Python License
|
||||||
|
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||||
|
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||||
|
and/or display publicly, prepare derivative works, distribute, and
|
||||||
|
otherwise use the Software alone or in any derivative version,
|
||||||
|
provided, however, that the BeOpen Python License is retained in the
|
||||||
|
Software, alone or in any derivative version prepared by Licensee.
|
||||||
|
|
||||||
|
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||||
|
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||||
|
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||||
|
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||||
|
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
5. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
6. This License Agreement shall be governed by and interpreted in all
|
||||||
|
respects by the law of the State of California, excluding conflict of
|
||||||
|
law provisions. Nothing in this License Agreement shall be deemed to
|
||||||
|
create any relationship of agency, partnership, or joint venture
|
||||||
|
between BeOpen and Licensee. This License Agreement does not grant
|
||||||
|
permission to use BeOpen trademarks or trade names in a trademark
|
||||||
|
sense to endorse or promote products or services of Licensee, or any
|
||||||
|
third party. As an exception, the "BeOpen Python" logos available at
|
||||||
|
http://www.pythonlabs.com/logos.html may be used according to the
|
||||||
|
permissions granted on that web page.
|
||||||
|
|
||||||
|
7. By copying, installing or otherwise using the software, Licensee
|
||||||
|
agrees to be bound by the terms and conditions of this License
|
||||||
|
Agreement.
|
||||||
|
|
||||||
|
|
||||||
|
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||||
|
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||||
|
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||||
|
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||||
|
source or binary form and its associated documentation.
|
||||||
|
|
||||||
|
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||||
|
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||||
|
license to reproduce, analyze, test, perform and/or display publicly,
|
||||||
|
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||||
|
alone or in any derivative version, provided, however, that CNRI's
|
||||||
|
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||||
|
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||||
|
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||||
|
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||||
|
Agreement, Licensee may substitute the following text (omitting the
|
||||||
|
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||||
|
conditions in CNRI's License Agreement. This Agreement together with
|
||||||
|
Python 1.6.1 may be located on the internet using the following
|
||||||
|
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||||
|
Agreement may also be obtained from a proxy server on the internet
|
||||||
|
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||||
|
|
||||||
|
3. In the event Licensee prepares a derivative work that is based on
|
||||||
|
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||||
|
the derivative work available to others as provided herein, then
|
||||||
|
Licensee hereby agrees to include in any such work a brief summary of
|
||||||
|
the changes made to Python 1.6.1.
|
||||||
|
|
||||||
|
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||||
|
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||||
|
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||||
|
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||||
|
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||||
|
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||||
|
|
||||||
|
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||||
|
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||||
|
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||||
|
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||||
|
|
||||||
|
6. This License Agreement will automatically terminate upon a material
|
||||||
|
breach of its terms and conditions.
|
||||||
|
|
||||||
|
7. This License Agreement shall be governed by the federal
|
||||||
|
intellectual property law of the United States, including without
|
||||||
|
limitation the federal copyright law, and, to the extent such
|
||||||
|
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||||
|
Virginia, excluding Virginia's conflict of law provisions.
|
||||||
|
Notwithstanding the foregoing, with regard to derivative works based
|
||||||
|
on Python 1.6.1 that incorporate non-separable material that was
|
||||||
|
previously distributed under the GNU General Public License (GPL), the
|
||||||
|
law of the Commonwealth of Virginia shall govern this License
|
||||||
|
Agreement only as to issues arising under or with respect to
|
||||||
|
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||||
|
License Agreement shall be deemed to create any relationship of
|
||||||
|
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||||
|
License Agreement does not grant permission to use CNRI trademarks or
|
||||||
|
trade name in a trademark sense to endorse or promote products or
|
||||||
|
services of Licensee, or any third party.
|
||||||
|
|
||||||
|
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||||
|
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||||
|
bound by the terms and conditions of this License Agreement.
|
||||||
|
|
||||||
|
ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||||
|
The Netherlands. All rights reserved.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and distribute this software and its
|
||||||
|
documentation for any purpose and without fee is hereby granted,
|
||||||
|
provided that the above copyright notice appear in all copies and that
|
||||||
|
both that copyright notice and this permission notice appear in
|
||||||
|
supporting documentation, and that the name of Stichting Mathematisch
|
||||||
|
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||||
|
distribution of the software without specific, written prior
|
||||||
|
permission.
|
||||||
|
|
||||||
|
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||||
|
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||||
|
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||||
|
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||||
|
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
PERFORMANCE OF THIS SOFTWARE.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
This software is under the MIT Licence
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Copyright (c) 2010 openpyxl
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: et_xmlfile
|
||||||
|
Version: 2.0.0
|
||||||
|
Summary: An implementation of lxml.xmlfile for the standard library
|
||||||
|
Home-page: https://foss.heptapod.net/openpyxl/et_xmlfile
|
||||||
|
Author: See AUTHORS.txt
|
||||||
|
Author-email: charlie.clark@clark-consulting.eu
|
||||||
|
License: MIT
|
||||||
|
Project-URL: Documentation, https://openpyxl.pages.heptapod.net/et_xmlfile/
|
||||||
|
Project-URL: Source, https://foss.heptapod.net/openpyxl/et_xmlfile
|
||||||
|
Project-URL: Tracker, https://foss.heptapod.net/openpyxl/et_xmfile/-/issues
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Operating System :: MacOS :: MacOS X
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Operating System :: POSIX
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Classifier: Programming Language :: Python :: 3.12
|
||||||
|
Classifier: Programming Language :: Python :: 3.13
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
License-File: LICENCE.python
|
||||||
|
License-File: LICENCE.rst
|
||||||
|
License-File: AUTHORS.txt
|
||||||
|
|
||||||
|
.. image:: https://foss.heptapod.net/openpyxl/et_xmlfile/badges/branch/default/coverage.svg
|
||||||
|
:target: https://coveralls.io/bitbucket/openpyxl/et_xmlfile?branch=default
|
||||||
|
:alt: coverage status
|
||||||
|
|
||||||
|
et_xmfile
|
||||||
|
=========
|
||||||
|
|
||||||
|
XML can use lots of memory, and et_xmlfile is a low memory library for creating large XML files
|
||||||
|
And, although the standard library already includes an incremental parser, `iterparse` it has no equivalent when writing XML. Once an element has been added to the tree, it is written to
|
||||||
|
the file or stream and the memory is then cleared.
|
||||||
|
|
||||||
|
This module is based upon the `xmlfile module from lxml <http://lxml.de/api.html#incremental-xml-generation>`_ with the aim of allowing code to be developed that will work with both libraries.
|
||||||
|
It was developed initially for the openpyxl project, but is now a standalone module.
|
||||||
|
|
||||||
|
The code was written by Elias Rabel as part of the `Python Düsseldorf <http://pyddf.de>`_ openpyxl sprint in September 2014.
|
||||||
|
|
||||||
|
Proper support for incremental writing was provided by Daniel Hillier in 2024
|
||||||
|
|
||||||
|
Note on performance
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The code was not developed with performance in mind, but turned out to be faster than the existing SAX-based implementation but is generally slower than lxml's xmlfile.
|
||||||
|
There is one area where an optimisation for lxml may negatively affect the performance of et_xmfile and that is when using the `.element()` method on the xmlfile context manager. It is, therefore, recommended simply to create Elements write these directly, as in the sample code.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
et_xmlfile-2.0.0.dist-info/AUTHORS.txt,sha256=fwOAKepUY2Bd0ieNMACZo4G86ekN2oPMqyBCNGtsgQc,82
|
||||||
|
et_xmlfile-2.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
et_xmlfile-2.0.0.dist-info/LICENCE.python,sha256=TM2q68D0S4NyDsA5m7erMprc4GfdYvc8VTWi3AViirI,14688
|
||||||
|
et_xmlfile-2.0.0.dist-info/LICENCE.rst,sha256=DIS7QvXTZ-Xr-fwt3jWxYUHfXuD9wYklCFi8bFVg9p4,1131
|
||||||
|
et_xmlfile-2.0.0.dist-info/METADATA,sha256=DpfX6pCe0PvgPYi8i29YZ3zuGwe9M1PONhzSQFkVIE4,2711
|
||||||
|
et_xmlfile-2.0.0.dist-info/RECORD,,
|
||||||
|
et_xmlfile-2.0.0.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
|
||||||
|
et_xmlfile-2.0.0.dist-info/top_level.txt,sha256=34-74d5NNARgTsPxCMta5o28XpBNmSN0iCZhtmx2Fk8,11
|
||||||
|
et_xmlfile/__init__.py,sha256=AQ4_2cNUEyUHlHo-Y3Gd6-8S_6eyKd55jYO4eh23UHw,228
|
||||||
|
et_xmlfile/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
et_xmlfile/__pycache__/incremental_tree.cpython-312.pyc,,
|
||||||
|
et_xmlfile/__pycache__/xmlfile.cpython-312.pyc,,
|
||||||
|
et_xmlfile/incremental_tree.py,sha256=lX4VStfzUNK0jtrVsvshPENu7E_zQirglkyRtzGDwEg,34534
|
||||||
|
et_xmlfile/xmlfile.py,sha256=6QdxBq2P0Cf35R-oyXjLl5wOItfJJ4Yy6AlIF9RX7Bg,4886
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: setuptools (72.2.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
et_xmlfile
|
||||||
8
venv/lib/python3.12/site-packages/et_xmlfile/__init__.py
Normal file
8
venv/lib/python3.12/site-packages/et_xmlfile/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from .xmlfile import xmlfile
|
||||||
|
|
||||||
|
# constants
|
||||||
|
__version__ = '2.0.0'
|
||||||
|
__author__ = 'See AUTHORS.txt'
|
||||||
|
__license__ = 'MIT'
|
||||||
|
__author_email__ = 'charlie.clark@clark-consulting.eu'
|
||||||
|
__url__ = 'https://foss.heptapod.net/openpyxl/et_xmlfile'
|
||||||
917
venv/lib/python3.12/site-packages/et_xmlfile/incremental_tree.py
Normal file
917
venv/lib/python3.12/site-packages/et_xmlfile/incremental_tree.py
Normal file
@@ -0,0 +1,917 @@
|
|||||||
|
# Code modified from cPython's Lib/xml/etree/ElementTree.py
|
||||||
|
# The write() code is modified to allow specifying a particular namespace
|
||||||
|
# uri -> prefix mapping.
|
||||||
|
#
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Licensed to PSF under a Contributor Agreement.
|
||||||
|
# See https://www.python.org/psf/license for licensing details.
|
||||||
|
#
|
||||||
|
# ElementTree
|
||||||
|
# Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved.
|
||||||
|
#
|
||||||
|
# fredrik@pythonware.com
|
||||||
|
# http://www.pythonware.com
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# The ElementTree toolkit is
|
||||||
|
#
|
||||||
|
# Copyright (c) 1999-2008 by Fredrik Lundh
|
||||||
|
#
|
||||||
|
# By obtaining, using, and/or copying this software and/or its
|
||||||
|
# associated documentation, you agree that you have read, understood,
|
||||||
|
# and will comply with the following terms and conditions:
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and distribute this software and
|
||||||
|
# its associated documentation for any purpose and without fee is
|
||||||
|
# hereby granted, provided that the above copyright notice appears in
|
||||||
|
# all copies, and that both that copyright notice and this permission
|
||||||
|
# notice appear in supporting documentation, and that the name of
|
||||||
|
# Secret Labs AB or the author not be used in advertising or publicity
|
||||||
|
# pertaining to distribution of the software without specific, written
|
||||||
|
# prior permission.
|
||||||
|
#
|
||||||
|
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||||
|
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||||
|
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||||
|
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||||
|
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||||
|
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||||
|
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||||
|
# OF THIS SOFTWARE.
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
def current_global_nsmap():
|
||||||
|
return {
|
||||||
|
prefix: uri for uri, prefix in ET._namespace_map.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IncrementalTree(ET.ElementTree):
|
||||||
|
|
||||||
|
def write(
|
||||||
|
self,
|
||||||
|
file_or_filename,
|
||||||
|
encoding=None,
|
||||||
|
xml_declaration=None,
|
||||||
|
default_namespace=None,
|
||||||
|
method=None,
|
||||||
|
*,
|
||||||
|
short_empty_elements=True,
|
||||||
|
nsmap=None,
|
||||||
|
root_ns_only=False,
|
||||||
|
minimal_ns_only=False,
|
||||||
|
):
|
||||||
|
"""Write element tree to a file as XML.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
*file_or_filename* -- file name or a file object opened for writing
|
||||||
|
|
||||||
|
*encoding* -- the output encoding (default: US-ASCII)
|
||||||
|
|
||||||
|
*xml_declaration* -- bool indicating if an XML declaration should be
|
||||||
|
added to the output. If None, an XML declaration
|
||||||
|
is added if encoding IS NOT either of:
|
||||||
|
US-ASCII, UTF-8, or Unicode
|
||||||
|
|
||||||
|
*default_namespace* -- sets the default XML namespace (for "xmlns").
|
||||||
|
Takes precedence over any default namespace
|
||||||
|
provided in nsmap or
|
||||||
|
xml.etree.ElementTree.register_namespace().
|
||||||
|
|
||||||
|
*method* -- either "xml" (default), "html, "text", or "c14n"
|
||||||
|
|
||||||
|
*short_empty_elements* -- controls the formatting of elements
|
||||||
|
that contain no content. If True (default)
|
||||||
|
they are emitted as a single self-closed
|
||||||
|
tag, otherwise they are emitted as a pair
|
||||||
|
of start/end tags
|
||||||
|
|
||||||
|
*nsmap* -- a mapping of namespace prefixes to URIs. These take
|
||||||
|
precedence over any mappings registered using
|
||||||
|
xml.etree.ElementTree.register_namespace(). The
|
||||||
|
default_namespace argument, if supplied, takes precedence
|
||||||
|
over any default namespace supplied in nsmap. All supplied
|
||||||
|
namespaces will be declared on the root element, even if
|
||||||
|
unused in the document.
|
||||||
|
|
||||||
|
*root_ns_only* -- bool indicating namespace declrations should only
|
||||||
|
be written on the root element. This requires two
|
||||||
|
passes of the xml tree adding additional time to
|
||||||
|
the writing process. This is primarily meant to
|
||||||
|
mimic xml.etree.ElementTree's behaviour.
|
||||||
|
|
||||||
|
*minimal_ns_only* -- bool indicating only namespaces that were used
|
||||||
|
to qualify elements or attributes should be
|
||||||
|
declared. All namespace declarations will be
|
||||||
|
written on the root element regardless of the
|
||||||
|
value of the root_ns_only arg. Requires two
|
||||||
|
passes of the xml tree adding additional time to
|
||||||
|
the writing process.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not method:
|
||||||
|
method = "xml"
|
||||||
|
elif method not in ("text", "xml", "html"):
|
||||||
|
raise ValueError("unknown method %r" % method)
|
||||||
|
if not encoding:
|
||||||
|
encoding = "us-ascii"
|
||||||
|
|
||||||
|
with _get_writer(file_or_filename, encoding) as (write, declared_encoding):
|
||||||
|
if method == "xml" and (
|
||||||
|
xml_declaration
|
||||||
|
or (
|
||||||
|
xml_declaration is None
|
||||||
|
and encoding.lower() != "unicode"
|
||||||
|
and declared_encoding.lower() not in ("utf-8", "us-ascii")
|
||||||
|
)
|
||||||
|
):
|
||||||
|
write("<?xml version='1.0' encoding='%s'?>\n" % (declared_encoding,))
|
||||||
|
if method == "text":
|
||||||
|
ET._serialize_text(write, self._root)
|
||||||
|
else:
|
||||||
|
if method == "xml":
|
||||||
|
is_html = False
|
||||||
|
else:
|
||||||
|
is_html = True
|
||||||
|
if nsmap:
|
||||||
|
if None in nsmap:
|
||||||
|
raise ValueError(
|
||||||
|
'Found None as default nsmap prefix in nsmap. '
|
||||||
|
'Use "" as the default namespace prefix.'
|
||||||
|
)
|
||||||
|
new_nsmap = nsmap.copy()
|
||||||
|
else:
|
||||||
|
new_nsmap = {}
|
||||||
|
if default_namespace:
|
||||||
|
new_nsmap[""] = default_namespace
|
||||||
|
if root_ns_only or minimal_ns_only:
|
||||||
|
# _namespaces returns a mapping of only the namespaces that
|
||||||
|
# were used.
|
||||||
|
new_nsmap = _namespaces(
|
||||||
|
self._root,
|
||||||
|
default_namespace,
|
||||||
|
new_nsmap,
|
||||||
|
)
|
||||||
|
if not minimal_ns_only:
|
||||||
|
if nsmap:
|
||||||
|
# We want all namespaces defined in the provided
|
||||||
|
# nsmap to be declared regardless of whether
|
||||||
|
# they've been used.
|
||||||
|
new_nsmap.update(nsmap)
|
||||||
|
if default_namespace:
|
||||||
|
new_nsmap[""] = default_namespace
|
||||||
|
global_nsmap = {
|
||||||
|
prefix: uri for uri, prefix in ET._namespace_map.items()
|
||||||
|
}
|
||||||
|
if None in global_nsmap:
|
||||||
|
raise ValueError(
|
||||||
|
'Found None as default nsmap prefix in nsmap registered with '
|
||||||
|
'register_namespace. Use "" for the default namespace prefix.'
|
||||||
|
)
|
||||||
|
nsmap_scope = {}
|
||||||
|
_serialize_ns_xml(
|
||||||
|
write,
|
||||||
|
self._root,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
is_html=is_html,
|
||||||
|
is_root=True,
|
||||||
|
short_empty_elements=short_empty_elements,
|
||||||
|
new_nsmap=new_nsmap,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_new_ns_prefix(
|
||||||
|
nsmap_scope,
|
||||||
|
global_prefixes,
|
||||||
|
local_nsmap=None,
|
||||||
|
default_namespace=None,
|
||||||
|
):
|
||||||
|
i = len(nsmap_scope)
|
||||||
|
if default_namespace is not None and "" not in nsmap_scope:
|
||||||
|
# Keep the same numbering scheme as python which assumes the default
|
||||||
|
# namespace is present if supplied.
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
prefix = f"ns{i}"
|
||||||
|
if (
|
||||||
|
prefix not in nsmap_scope
|
||||||
|
and prefix not in global_prefixes
|
||||||
|
and (
|
||||||
|
not local_nsmap or prefix not in local_nsmap
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return prefix
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _get_or_create_prefix(
|
||||||
|
uri,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
new_namespace_prefixes,
|
||||||
|
uri_to_prefix,
|
||||||
|
for_default_namespace_attr_prefix=False,
|
||||||
|
):
|
||||||
|
"""Find a prefix that doesn't conflict with the ns scope or create a new prefix
|
||||||
|
|
||||||
|
This function mutates nsmap_scope, global_nsmap, new_namespace_prefixes and
|
||||||
|
uri_to_prefix. It is intended to keep state in _serialize_ns_xml consistent
|
||||||
|
while deduplicating the house keeping code or updating these dictionaries.
|
||||||
|
"""
|
||||||
|
# Check if we can reuse an existing (global) prefix within the current
|
||||||
|
# namespace scope. There maybe many prefixes pointing to a single URI by
|
||||||
|
# this point and we need to select a prefix that is not in use in the
|
||||||
|
# current scope.
|
||||||
|
for global_prefix, global_uri in global_nsmap.items():
|
||||||
|
if uri == global_uri and global_prefix not in nsmap_scope:
|
||||||
|
prefix = global_prefix
|
||||||
|
break
|
||||||
|
else: # no break
|
||||||
|
# We couldn't find a suitable existing prefix for this namespace scope,
|
||||||
|
# let's create a new one.
|
||||||
|
prefix = _make_new_ns_prefix(nsmap_scope, global_prefixes=global_nsmap)
|
||||||
|
global_nsmap[prefix] = uri
|
||||||
|
nsmap_scope[prefix] = uri
|
||||||
|
if not for_default_namespace_attr_prefix:
|
||||||
|
# Don't override the actual default namespace prefix
|
||||||
|
uri_to_prefix[uri] = prefix
|
||||||
|
if prefix != "xml":
|
||||||
|
new_namespace_prefixes.add(prefix)
|
||||||
|
return prefix
|
||||||
|
|
||||||
|
|
||||||
|
def _find_default_namespace_attr_prefix(
|
||||||
|
default_namespace,
|
||||||
|
nsmap,
|
||||||
|
local_nsmap,
|
||||||
|
global_prefixes,
|
||||||
|
provided_default_namespace=None,
|
||||||
|
):
|
||||||
|
# Search the provided nsmap for any prefixes for this uri that aren't the
|
||||||
|
# default namespace ""
|
||||||
|
for prefix, uri in nsmap.items():
|
||||||
|
if uri == default_namespace and prefix != "":
|
||||||
|
return prefix
|
||||||
|
|
||||||
|
for prefix, uri in local_nsmap.items():
|
||||||
|
if uri == default_namespace and prefix != "":
|
||||||
|
return prefix
|
||||||
|
|
||||||
|
# _namespace_map is a 1:1 mapping of uri -> prefix
|
||||||
|
prefix = ET._namespace_map.get(default_namespace)
|
||||||
|
if prefix and prefix not in nsmap:
|
||||||
|
return prefix
|
||||||
|
|
||||||
|
return _make_new_ns_prefix(
|
||||||
|
nsmap,
|
||||||
|
global_prefixes,
|
||||||
|
local_nsmap,
|
||||||
|
provided_default_namespace,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def process_attribs(
|
||||||
|
elem,
|
||||||
|
is_nsmap_scope_changed,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
new_namespace_prefixes,
|
||||||
|
uri_to_prefix,
|
||||||
|
):
|
||||||
|
item_parts = []
|
||||||
|
for k, v in elem.items():
|
||||||
|
if isinstance(k, ET.QName):
|
||||||
|
k = k.text
|
||||||
|
try:
|
||||||
|
if k[:1] == "{":
|
||||||
|
uri_and_name = k[1:].rsplit("}", 1)
|
||||||
|
try:
|
||||||
|
prefix = uri_to_prefix[uri_and_name[0]]
|
||||||
|
except KeyError:
|
||||||
|
if not is_nsmap_scope_changed:
|
||||||
|
# We're about to mutate the these dicts so
|
||||||
|
# let's copy them first. We don't have to
|
||||||
|
# recompute other mappings as we're looking up
|
||||||
|
# or creating a new prefix
|
||||||
|
nsmap_scope = nsmap_scope.copy()
|
||||||
|
uri_to_prefix = uri_to_prefix.copy()
|
||||||
|
is_nsmap_scope_changed = True
|
||||||
|
prefix = _get_or_create_prefix(
|
||||||
|
uri_and_name[0],
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
new_namespace_prefixes,
|
||||||
|
uri_to_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not prefix:
|
||||||
|
if default_ns_attr_prefix:
|
||||||
|
prefix = default_ns_attr_prefix
|
||||||
|
else:
|
||||||
|
for prefix, known_uri in nsmap_scope.items():
|
||||||
|
if known_uri == uri_and_name[0] and prefix != "":
|
||||||
|
default_ns_attr_prefix = prefix
|
||||||
|
break
|
||||||
|
else: # no break
|
||||||
|
if not is_nsmap_scope_changed:
|
||||||
|
# We're about to mutate the these dicts so
|
||||||
|
# let's copy them first. We don't have to
|
||||||
|
# recompute other mappings as we're looking up
|
||||||
|
# or creating a new prefix
|
||||||
|
nsmap_scope = nsmap_scope.copy()
|
||||||
|
uri_to_prefix = uri_to_prefix.copy()
|
||||||
|
is_nsmap_scope_changed = True
|
||||||
|
prefix = _get_or_create_prefix(
|
||||||
|
uri_and_name[0],
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
new_namespace_prefixes,
|
||||||
|
uri_to_prefix,
|
||||||
|
for_default_namespace_attr_prefix=True,
|
||||||
|
)
|
||||||
|
default_ns_attr_prefix = prefix
|
||||||
|
k = f"{prefix}:{uri_and_name[1]}"
|
||||||
|
except TypeError:
|
||||||
|
ET._raise_serialization_error(k)
|
||||||
|
|
||||||
|
if isinstance(v, ET.QName):
|
||||||
|
if v.text[:1] != "{":
|
||||||
|
v = v.text
|
||||||
|
else:
|
||||||
|
uri_and_name = v.text[1:].rsplit("}", 1)
|
||||||
|
try:
|
||||||
|
prefix = uri_to_prefix[uri_and_name[0]]
|
||||||
|
except KeyError:
|
||||||
|
if not is_nsmap_scope_changed:
|
||||||
|
# We're about to mutate the these dicts so
|
||||||
|
# let's copy them first. We don't have to
|
||||||
|
# recompute other mappings as we're looking up
|
||||||
|
# or creating a new prefix
|
||||||
|
nsmap_scope = nsmap_scope.copy()
|
||||||
|
uri_to_prefix = uri_to_prefix.copy()
|
||||||
|
is_nsmap_scope_changed = True
|
||||||
|
prefix = _get_or_create_prefix(
|
||||||
|
uri_and_name[0],
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
new_namespace_prefixes,
|
||||||
|
uri_to_prefix,
|
||||||
|
)
|
||||||
|
v = f"{prefix}:{uri_and_name[1]}"
|
||||||
|
item_parts.append((k, v))
|
||||||
|
return item_parts, default_ns_attr_prefix, nsmap_scope
|
||||||
|
|
||||||
|
|
||||||
|
def write_elem_start(
|
||||||
|
write,
|
||||||
|
elem,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
short_empty_elements,
|
||||||
|
is_html,
|
||||||
|
is_root=False,
|
||||||
|
uri_to_prefix=None,
|
||||||
|
default_ns_attr_prefix=None,
|
||||||
|
new_nsmap=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Write the opening tag (including self closing) and element text.
|
||||||
|
|
||||||
|
Refer to _serialize_ns_xml for description of arguments.
|
||||||
|
|
||||||
|
nsmap_scope should be an empty dictionary on first call. All nsmap prefixes
|
||||||
|
must be strings with the default namespace prefix represented by "".
|
||||||
|
|
||||||
|
eg.
|
||||||
|
- <foo attr1="one"> (returns tag = 'foo')
|
||||||
|
- <foo attr1="one">text (returns tag = 'foo')
|
||||||
|
- <foo attr1="one" /> (returns tag = None)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tag:
|
||||||
|
The tag name to be closed or None if no closing required.
|
||||||
|
nsmap_scope:
|
||||||
|
The current nsmap after any prefix to uri additions from this
|
||||||
|
element. This is the input dict if unmodified or an updated copy.
|
||||||
|
default_ns_attr_prefix:
|
||||||
|
The prefix for the default namespace to use with attrs.
|
||||||
|
uri_to_prefix:
|
||||||
|
The current uri to prefix map after any uri to prefix additions
|
||||||
|
from this element. This is the input dict if unmodified or an
|
||||||
|
updated copy.
|
||||||
|
next_remains_root:
|
||||||
|
A bool indicating if the child element(s) should be treated as
|
||||||
|
their own roots.
|
||||||
|
"""
|
||||||
|
tag = elem.tag
|
||||||
|
text = elem.text
|
||||||
|
|
||||||
|
if tag is ET.Comment:
|
||||||
|
write("<!--%s-->" % text)
|
||||||
|
tag = None
|
||||||
|
next_remains_root = False
|
||||||
|
elif tag is ET.ProcessingInstruction:
|
||||||
|
write("<?%s?>" % text)
|
||||||
|
tag = None
|
||||||
|
next_remains_root = False
|
||||||
|
else:
|
||||||
|
if new_nsmap:
|
||||||
|
is_nsmap_scope_changed = True
|
||||||
|
nsmap_scope = nsmap_scope.copy()
|
||||||
|
nsmap_scope.update(new_nsmap)
|
||||||
|
new_namespace_prefixes = set(new_nsmap.keys())
|
||||||
|
new_namespace_prefixes.discard("xml")
|
||||||
|
# We need to recompute the uri to prefixes
|
||||||
|
uri_to_prefix = None
|
||||||
|
default_ns_attr_prefix = None
|
||||||
|
else:
|
||||||
|
is_nsmap_scope_changed = False
|
||||||
|
new_namespace_prefixes = set()
|
||||||
|
|
||||||
|
if uri_to_prefix is None:
|
||||||
|
if None in nsmap_scope:
|
||||||
|
raise ValueError(
|
||||||
|
'Found None as a namespace prefix. Use "" as the default namespace prefix.'
|
||||||
|
)
|
||||||
|
uri_to_prefix = {uri: prefix for prefix, uri in nsmap_scope.items()}
|
||||||
|
if "" in nsmap_scope:
|
||||||
|
# There may be multiple prefixes for the default namespace but
|
||||||
|
# we want to make sure we preferentially use "" (for elements)
|
||||||
|
uri_to_prefix[nsmap_scope[""]] = ""
|
||||||
|
|
||||||
|
if tag is None:
|
||||||
|
# tag supression where tag is set to None
|
||||||
|
# Don't change is_root so namespaces can be passed down
|
||||||
|
next_remains_root = is_root
|
||||||
|
if text:
|
||||||
|
write(ET._escape_cdata(text))
|
||||||
|
else:
|
||||||
|
next_remains_root = False
|
||||||
|
if isinstance(tag, ET.QName):
|
||||||
|
tag = tag.text
|
||||||
|
try:
|
||||||
|
# These splits / fully qualified tag creationg are the
|
||||||
|
# bottleneck in this implementation vs the python
|
||||||
|
# implementation.
|
||||||
|
# The following split takes ~42ns with no uri and ~85ns if a
|
||||||
|
# prefix is present. If the uri was present, we then need to
|
||||||
|
# look up a prefix (~14ns) and create the fully qualified
|
||||||
|
# string (~41ns). This gives a total of ~140ns where a uri is
|
||||||
|
# present.
|
||||||
|
# Python's implementation needs to preprocess the tree to
|
||||||
|
# create a dict of qname -> tag by traversing the tree which
|
||||||
|
# takes a bit of extra time but it quickly makes that back by
|
||||||
|
# only having to do a dictionary look up (~14ns) for each tag /
|
||||||
|
# attrname vs our splitting (~140ns).
|
||||||
|
# So here we have the flexibility of being able to redefine the
|
||||||
|
# uri a prefix points to midway through serialisation at the
|
||||||
|
# expense of performance (~10% slower for a 1mb file on my
|
||||||
|
# machine).
|
||||||
|
if tag[:1] == "{":
|
||||||
|
uri_and_name = tag[1:].rsplit("}", 1)
|
||||||
|
try:
|
||||||
|
prefix = uri_to_prefix[uri_and_name[0]]
|
||||||
|
except KeyError:
|
||||||
|
if not is_nsmap_scope_changed:
|
||||||
|
# We're about to mutate the these dicts so let's
|
||||||
|
# copy them first. We don't have to recompute other
|
||||||
|
# mappings as we're looking up or creating a new
|
||||||
|
# prefix
|
||||||
|
nsmap_scope = nsmap_scope.copy()
|
||||||
|
uri_to_prefix = uri_to_prefix.copy()
|
||||||
|
is_nsmap_scope_changed = True
|
||||||
|
prefix = _get_or_create_prefix(
|
||||||
|
uri_and_name[0],
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
new_namespace_prefixes,
|
||||||
|
uri_to_prefix,
|
||||||
|
)
|
||||||
|
if prefix:
|
||||||
|
tag = f"{prefix}:{uri_and_name[1]}"
|
||||||
|
else:
|
||||||
|
tag = uri_and_name[1]
|
||||||
|
elif "" in nsmap_scope:
|
||||||
|
raise ValueError(
|
||||||
|
"cannot use non-qualified names with default_namespace option"
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
ET._raise_serialization_error(tag)
|
||||||
|
|
||||||
|
write("<" + tag)
|
||||||
|
|
||||||
|
if elem.attrib:
|
||||||
|
item_parts, default_ns_attr_prefix, nsmap_scope = process_attribs(
|
||||||
|
elem,
|
||||||
|
is_nsmap_scope_changed,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
new_namespace_prefixes,
|
||||||
|
uri_to_prefix,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
item_parts = []
|
||||||
|
if new_namespace_prefixes:
|
||||||
|
ns_attrs = []
|
||||||
|
for k in sorted(new_namespace_prefixes):
|
||||||
|
v = nsmap_scope[k]
|
||||||
|
if k:
|
||||||
|
k = "xmlns:" + k
|
||||||
|
else:
|
||||||
|
k = "xmlns"
|
||||||
|
ns_attrs.append((k, v))
|
||||||
|
if is_html:
|
||||||
|
write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in ns_attrs]))
|
||||||
|
else:
|
||||||
|
write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in ns_attrs]))
|
||||||
|
if item_parts:
|
||||||
|
if is_html:
|
||||||
|
write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in item_parts]))
|
||||||
|
else:
|
||||||
|
write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in item_parts]))
|
||||||
|
if is_html:
|
||||||
|
write(">")
|
||||||
|
ltag = tag.lower()
|
||||||
|
if text:
|
||||||
|
if ltag == "script" or ltag == "style":
|
||||||
|
write(text)
|
||||||
|
else:
|
||||||
|
write(ET._escape_cdata(text))
|
||||||
|
if ltag in ET.HTML_EMPTY:
|
||||||
|
tag = None
|
||||||
|
elif text or len(elem) or not short_empty_elements:
|
||||||
|
write(">")
|
||||||
|
if text:
|
||||||
|
write(ET._escape_cdata(text))
|
||||||
|
else:
|
||||||
|
tag = None
|
||||||
|
write(" />")
|
||||||
|
return (
|
||||||
|
tag,
|
||||||
|
nsmap_scope,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
uri_to_prefix,
|
||||||
|
next_remains_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_ns_xml(
|
||||||
|
write,
|
||||||
|
elem,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
short_empty_elements,
|
||||||
|
is_html,
|
||||||
|
is_root=False,
|
||||||
|
uri_to_prefix=None,
|
||||||
|
default_ns_attr_prefix=None,
|
||||||
|
new_nsmap=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Serialize an element or tree using 'write' for output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
write:
|
||||||
|
A function to write the xml to its destination.
|
||||||
|
elem:
|
||||||
|
The element to serialize.
|
||||||
|
nsmap_scope:
|
||||||
|
The current prefix to uri mapping for this element. This should be
|
||||||
|
an empty dictionary for the root element. Additional namespaces are
|
||||||
|
progressively added using the new_nsmap arg.
|
||||||
|
global_nsmap:
|
||||||
|
A dict copy of the globally registered _namespace_map in uri to
|
||||||
|
prefix form
|
||||||
|
short_empty_elements:
|
||||||
|
Controls the formatting of elements that contain no content. If True
|
||||||
|
(default) they are emitted as a single self-closed tag, otherwise
|
||||||
|
they are emitted as a pair of start/end tags.
|
||||||
|
is_html:
|
||||||
|
Set to True to serialize as HTML otherwise XML.
|
||||||
|
is_root:
|
||||||
|
Boolean indicating if this is a root element.
|
||||||
|
uri_to_prefix:
|
||||||
|
Current state of the mapping of uri to prefix.
|
||||||
|
default_ns_attr_prefix:
|
||||||
|
new_nsmap:
|
||||||
|
New prefix -> uri mapping to be applied to this element.
|
||||||
|
"""
|
||||||
|
(
|
||||||
|
tag,
|
||||||
|
nsmap_scope,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
uri_to_prefix,
|
||||||
|
next_remains_root,
|
||||||
|
) = write_elem_start(
|
||||||
|
write,
|
||||||
|
elem,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
short_empty_elements,
|
||||||
|
is_html,
|
||||||
|
is_root,
|
||||||
|
uri_to_prefix,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
new_nsmap=new_nsmap,
|
||||||
|
)
|
||||||
|
for e in elem:
|
||||||
|
_serialize_ns_xml(
|
||||||
|
write,
|
||||||
|
e,
|
||||||
|
nsmap_scope,
|
||||||
|
global_nsmap,
|
||||||
|
short_empty_elements,
|
||||||
|
is_html,
|
||||||
|
next_remains_root,
|
||||||
|
uri_to_prefix,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
new_nsmap=None,
|
||||||
|
)
|
||||||
|
if tag:
|
||||||
|
write(f"</{tag}>")
|
||||||
|
if elem.tail:
|
||||||
|
write(ET._escape_cdata(elem.tail))
|
||||||
|
|
||||||
|
|
||||||
|
def _qnames_iter(elem):
|
||||||
|
"""Iterate through all the qualified names in elem"""
|
||||||
|
seen_el_qnames = set()
|
||||||
|
seen_other_qnames = set()
|
||||||
|
for this_elem in elem.iter():
|
||||||
|
tag = this_elem.tag
|
||||||
|
if isinstance(tag, str):
|
||||||
|
if tag not in seen_el_qnames:
|
||||||
|
seen_el_qnames.add(tag)
|
||||||
|
yield tag, True
|
||||||
|
elif isinstance(tag, ET.QName):
|
||||||
|
tag = tag.text
|
||||||
|
if tag not in seen_el_qnames:
|
||||||
|
seen_el_qnames.add(tag)
|
||||||
|
yield tag, True
|
||||||
|
elif (
|
||||||
|
tag is not None
|
||||||
|
and tag is not ET.ProcessingInstruction
|
||||||
|
and tag is not ET.Comment
|
||||||
|
):
|
||||||
|
ET._raise_serialization_error(tag)
|
||||||
|
|
||||||
|
for key, value in this_elem.items():
|
||||||
|
if isinstance(key, ET.QName):
|
||||||
|
key = key.text
|
||||||
|
if key not in seen_other_qnames:
|
||||||
|
seen_other_qnames.add(key)
|
||||||
|
yield key, False
|
||||||
|
|
||||||
|
if isinstance(value, ET.QName):
|
||||||
|
if value.text not in seen_other_qnames:
|
||||||
|
seen_other_qnames.add(value.text)
|
||||||
|
yield value.text, False
|
||||||
|
|
||||||
|
text = this_elem.text
|
||||||
|
if isinstance(text, ET.QName):
|
||||||
|
if text.text not in seen_other_qnames:
|
||||||
|
seen_other_qnames.add(text.text)
|
||||||
|
yield text.text, False
|
||||||
|
|
||||||
|
|
||||||
|
def _namespaces(
|
||||||
|
elem,
|
||||||
|
default_namespace=None,
|
||||||
|
nsmap=None,
|
||||||
|
):
|
||||||
|
"""Find all namespaces used in the document and return a prefix to uri map"""
|
||||||
|
if nsmap is None:
|
||||||
|
nsmap = {}
|
||||||
|
|
||||||
|
out_nsmap = {}
|
||||||
|
|
||||||
|
seen_uri_to_prefix = {}
|
||||||
|
# Multiple prefixes may be present for a single uri. This will select the
|
||||||
|
# last prefix found in nsmap for a given uri.
|
||||||
|
local_prefix_map = {uri: prefix for prefix, uri in nsmap.items()}
|
||||||
|
if default_namespace is not None:
|
||||||
|
local_prefix_map[default_namespace] = ""
|
||||||
|
elif "" in nsmap:
|
||||||
|
# but we make sure the default prefix always take precedence
|
||||||
|
local_prefix_map[nsmap[""]] = ""
|
||||||
|
|
||||||
|
global_prefixes = set(ET._namespace_map.values())
|
||||||
|
has_unqual_el = False
|
||||||
|
default_namespace_attr_prefix = None
|
||||||
|
for qname, is_el in _qnames_iter(elem):
|
||||||
|
try:
|
||||||
|
if qname[:1] == "{":
|
||||||
|
uri_and_name = qname[1:].rsplit("}", 1)
|
||||||
|
|
||||||
|
prefix = seen_uri_to_prefix.get(uri_and_name[0])
|
||||||
|
if prefix is None:
|
||||||
|
prefix = local_prefix_map.get(uri_and_name[0])
|
||||||
|
if prefix is None or prefix in out_nsmap:
|
||||||
|
prefix = ET._namespace_map.get(uri_and_name[0])
|
||||||
|
if prefix is None or prefix in out_nsmap:
|
||||||
|
prefix = _make_new_ns_prefix(
|
||||||
|
out_nsmap,
|
||||||
|
global_prefixes,
|
||||||
|
nsmap,
|
||||||
|
default_namespace,
|
||||||
|
)
|
||||||
|
if prefix or is_el:
|
||||||
|
out_nsmap[prefix] = uri_and_name[0]
|
||||||
|
seen_uri_to_prefix[uri_and_name[0]] = prefix
|
||||||
|
|
||||||
|
if not is_el and not prefix and not default_namespace_attr_prefix:
|
||||||
|
# Find the alternative prefix to use with non-element
|
||||||
|
# names
|
||||||
|
default_namespace_attr_prefix = _find_default_namespace_attr_prefix(
|
||||||
|
uri_and_name[0],
|
||||||
|
out_nsmap,
|
||||||
|
nsmap,
|
||||||
|
global_prefixes,
|
||||||
|
default_namespace,
|
||||||
|
)
|
||||||
|
out_nsmap[default_namespace_attr_prefix] = uri_and_name[0]
|
||||||
|
# Don't add this uri to prefix mapping as it might override
|
||||||
|
# the uri -> "" default mapping. We'll fix this up at the
|
||||||
|
# end of the fn.
|
||||||
|
# local_prefix_map[uri_and_name[0]] = default_namespace_attr_prefix
|
||||||
|
else:
|
||||||
|
if is_el:
|
||||||
|
has_unqual_el = True
|
||||||
|
except TypeError:
|
||||||
|
ET._raise_serialization_error(qname)
|
||||||
|
|
||||||
|
if "" in out_nsmap and has_unqual_el:
|
||||||
|
# FIXME: can this be handled in XML 1.0?
|
||||||
|
raise ValueError(
|
||||||
|
"cannot use non-qualified names with default_namespace option"
|
||||||
|
)
|
||||||
|
|
||||||
|
# The xml prefix doesn't need to be declared but may have been used to
|
||||||
|
# prefix names. Let's remove it if it has been used
|
||||||
|
out_nsmap.pop("xml", None)
|
||||||
|
return out_nsmap
|
||||||
|
|
||||||
|
|
||||||
|
def tostring(
|
||||||
|
element,
|
||||||
|
encoding=None,
|
||||||
|
method=None,
|
||||||
|
*,
|
||||||
|
xml_declaration=None,
|
||||||
|
default_namespace=None,
|
||||||
|
short_empty_elements=True,
|
||||||
|
nsmap=None,
|
||||||
|
root_ns_only=False,
|
||||||
|
minimal_ns_only=False,
|
||||||
|
tree_cls=IncrementalTree,
|
||||||
|
):
|
||||||
|
"""Generate string representation of XML element.
|
||||||
|
|
||||||
|
All subelements are included. If encoding is "unicode", a string
|
||||||
|
is returned. Otherwise a bytestring is returned.
|
||||||
|
|
||||||
|
*element* is an Element instance, *encoding* is an optional output
|
||||||
|
encoding defaulting to US-ASCII, *method* is an optional output which can
|
||||||
|
be one of "xml" (default), "html", "text" or "c14n", *default_namespace*
|
||||||
|
sets the default XML namespace (for "xmlns").
|
||||||
|
|
||||||
|
Returns an (optionally) encoded string containing the XML data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
stream = io.StringIO() if encoding == "unicode" else io.BytesIO()
|
||||||
|
tree_cls(element).write(
|
||||||
|
stream,
|
||||||
|
encoding,
|
||||||
|
xml_declaration=xml_declaration,
|
||||||
|
default_namespace=default_namespace,
|
||||||
|
method=method,
|
||||||
|
short_empty_elements=short_empty_elements,
|
||||||
|
nsmap=nsmap,
|
||||||
|
root_ns_only=root_ns_only,
|
||||||
|
minimal_ns_only=minimal_ns_only,
|
||||||
|
)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def tostringlist(
|
||||||
|
element,
|
||||||
|
encoding=None,
|
||||||
|
method=None,
|
||||||
|
*,
|
||||||
|
xml_declaration=None,
|
||||||
|
default_namespace=None,
|
||||||
|
short_empty_elements=True,
|
||||||
|
nsmap=None,
|
||||||
|
root_ns_only=False,
|
||||||
|
minimal_ns_only=False,
|
||||||
|
tree_cls=IncrementalTree,
|
||||||
|
):
|
||||||
|
lst = []
|
||||||
|
stream = ET._ListDataStream(lst)
|
||||||
|
tree_cls(element).write(
|
||||||
|
stream,
|
||||||
|
encoding,
|
||||||
|
xml_declaration=xml_declaration,
|
||||||
|
default_namespace=default_namespace,
|
||||||
|
method=method,
|
||||||
|
short_empty_elements=short_empty_elements,
|
||||||
|
nsmap=nsmap,
|
||||||
|
root_ns_only=root_ns_only,
|
||||||
|
minimal_ns_only=minimal_ns_only,
|
||||||
|
)
|
||||||
|
return lst
|
||||||
|
|
||||||
|
|
||||||
|
def compat_tostring(
|
||||||
|
element,
|
||||||
|
encoding=None,
|
||||||
|
method=None,
|
||||||
|
*,
|
||||||
|
xml_declaration=None,
|
||||||
|
default_namespace=None,
|
||||||
|
short_empty_elements=True,
|
||||||
|
nsmap=None,
|
||||||
|
root_ns_only=True,
|
||||||
|
minimal_ns_only=False,
|
||||||
|
tree_cls=IncrementalTree,
|
||||||
|
):
|
||||||
|
"""tostring with options that produce the same results as xml.etree.ElementTree.tostring
|
||||||
|
|
||||||
|
root_ns_only=True is a bit slower than False as it needs to traverse the
|
||||||
|
tree one more time to collect all the namespaces.
|
||||||
|
"""
|
||||||
|
return tostring(
|
||||||
|
element,
|
||||||
|
encoding=encoding,
|
||||||
|
method=method,
|
||||||
|
xml_declaration=xml_declaration,
|
||||||
|
default_namespace=default_namespace,
|
||||||
|
short_empty_elements=short_empty_elements,
|
||||||
|
nsmap=nsmap,
|
||||||
|
root_ns_only=root_ns_only,
|
||||||
|
minimal_ns_only=minimal_ns_only,
|
||||||
|
tree_cls=tree_cls,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# serialization support
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _get_writer(file_or_filename, encoding):
|
||||||
|
# Copied from Python 3.12
|
||||||
|
# returns text write method and release all resources after using
|
||||||
|
try:
|
||||||
|
write = file_or_filename.write
|
||||||
|
except AttributeError:
|
||||||
|
# file_or_filename is a file name
|
||||||
|
if encoding.lower() == "unicode":
|
||||||
|
encoding = "utf-8"
|
||||||
|
with open(file_or_filename, "w", encoding=encoding,
|
||||||
|
errors="xmlcharrefreplace") as file:
|
||||||
|
yield file.write, encoding
|
||||||
|
else:
|
||||||
|
# file_or_filename is a file-like object
|
||||||
|
# encoding determines if it is a text or binary writer
|
||||||
|
if encoding.lower() == "unicode":
|
||||||
|
# use a text writer as is
|
||||||
|
yield write, getattr(file_or_filename, "encoding", None) or "utf-8"
|
||||||
|
else:
|
||||||
|
# wrap a binary writer with TextIOWrapper
|
||||||
|
with contextlib.ExitStack() as stack:
|
||||||
|
if isinstance(file_or_filename, io.BufferedIOBase):
|
||||||
|
file = file_or_filename
|
||||||
|
elif isinstance(file_or_filename, io.RawIOBase):
|
||||||
|
file = io.BufferedWriter(file_or_filename)
|
||||||
|
# Keep the original file open when the BufferedWriter is
|
||||||
|
# destroyed
|
||||||
|
stack.callback(file.detach)
|
||||||
|
else:
|
||||||
|
# This is to handle passed objects that aren't in the
|
||||||
|
# IOBase hierarchy, but just have a write method
|
||||||
|
file = io.BufferedIOBase()
|
||||||
|
file.writable = lambda: True
|
||||||
|
file.write = write
|
||||||
|
try:
|
||||||
|
# TextIOWrapper uses this methods to determine
|
||||||
|
# if BOM (for UTF-16, etc) should be added
|
||||||
|
file.seekable = file_or_filename.seekable
|
||||||
|
file.tell = file_or_filename.tell
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
file = io.TextIOWrapper(file,
|
||||||
|
encoding=encoding,
|
||||||
|
errors="xmlcharrefreplace",
|
||||||
|
newline="\n")
|
||||||
|
# Keep the original file open when the TextIOWrapper is
|
||||||
|
# destroyed
|
||||||
|
stack.callback(file.detach)
|
||||||
|
yield file.write, encoding
|
||||||
158
venv/lib/python3.12/site-packages/et_xmlfile/xmlfile.py
Normal file
158
venv/lib/python3.12/site-packages/et_xmlfile/xmlfile.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
# Copyright (c) 2010-2015 openpyxl
|
||||||
|
|
||||||
|
"""Implements the lxml.etree.xmlfile API using the standard library xml.etree"""
|
||||||
|
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from xml.etree.ElementTree import (
|
||||||
|
Element,
|
||||||
|
_escape_cdata,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import incremental_tree
|
||||||
|
|
||||||
|
|
||||||
|
class LxmlSyntaxError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _IncrementalFileWriter(object):
|
||||||
|
"""Replacement for _IncrementalFileWriter of lxml"""
|
||||||
|
def __init__(self, output_file):
|
||||||
|
self._element_stack = []
|
||||||
|
self._file = output_file
|
||||||
|
self._have_root = False
|
||||||
|
self.global_nsmap = incremental_tree.current_global_nsmap()
|
||||||
|
self.is_html = False
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def element(self, tag, attrib=None, nsmap=None, **_extra):
|
||||||
|
"""Create a new xml element using a context manager."""
|
||||||
|
if nsmap and None in nsmap:
|
||||||
|
# Normalise None prefix (lxml's default namespace prefix) -> "", as
|
||||||
|
# required for incremental_tree
|
||||||
|
if "" in nsmap and nsmap[""] != nsmap[None]:
|
||||||
|
raise ValueError(
|
||||||
|
'Found None and "" as default nsmap prefixes with different URIs'
|
||||||
|
)
|
||||||
|
nsmap = nsmap.copy()
|
||||||
|
nsmap[""] = nsmap.pop(None)
|
||||||
|
|
||||||
|
# __enter__ part
|
||||||
|
self._have_root = True
|
||||||
|
if attrib is None:
|
||||||
|
attrib = {}
|
||||||
|
elem = Element(tag, attrib=attrib, **_extra)
|
||||||
|
elem.text = ''
|
||||||
|
elem.tail = ''
|
||||||
|
if self._element_stack:
|
||||||
|
is_root = False
|
||||||
|
(
|
||||||
|
nsmap_scope,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
uri_to_prefix,
|
||||||
|
) = self._element_stack[-1]
|
||||||
|
else:
|
||||||
|
is_root = True
|
||||||
|
nsmap_scope = {}
|
||||||
|
default_ns_attr_prefix = None
|
||||||
|
uri_to_prefix = {}
|
||||||
|
(
|
||||||
|
tag,
|
||||||
|
nsmap_scope,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
uri_to_prefix,
|
||||||
|
next_remains_root,
|
||||||
|
) = incremental_tree.write_elem_start(
|
||||||
|
self._file,
|
||||||
|
elem,
|
||||||
|
nsmap_scope=nsmap_scope,
|
||||||
|
global_nsmap=self.global_nsmap,
|
||||||
|
short_empty_elements=False,
|
||||||
|
is_html=self.is_html,
|
||||||
|
is_root=is_root,
|
||||||
|
uri_to_prefix=uri_to_prefix,
|
||||||
|
default_ns_attr_prefix=default_ns_attr_prefix,
|
||||||
|
new_nsmap=nsmap,
|
||||||
|
)
|
||||||
|
self._element_stack.append(
|
||||||
|
(
|
||||||
|
nsmap_scope,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
uri_to_prefix,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
yield
|
||||||
|
|
||||||
|
# __exit__ part
|
||||||
|
self._element_stack.pop()
|
||||||
|
self._file(f"</{tag}>")
|
||||||
|
if elem.tail:
|
||||||
|
self._file(_escape_cdata(elem.tail))
|
||||||
|
|
||||||
|
def write(self, arg):
|
||||||
|
"""Write a string or subelement."""
|
||||||
|
|
||||||
|
if isinstance(arg, str):
|
||||||
|
# it is not allowed to write a string outside of an element
|
||||||
|
if not self._element_stack:
|
||||||
|
raise LxmlSyntaxError()
|
||||||
|
self._file(_escape_cdata(arg))
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not self._element_stack and self._have_root:
|
||||||
|
raise LxmlSyntaxError()
|
||||||
|
|
||||||
|
if self._element_stack:
|
||||||
|
is_root = False
|
||||||
|
(
|
||||||
|
nsmap_scope,
|
||||||
|
default_ns_attr_prefix,
|
||||||
|
uri_to_prefix,
|
||||||
|
) = self._element_stack[-1]
|
||||||
|
else:
|
||||||
|
is_root = True
|
||||||
|
nsmap_scope = {}
|
||||||
|
default_ns_attr_prefix = None
|
||||||
|
uri_to_prefix = {}
|
||||||
|
incremental_tree._serialize_ns_xml(
|
||||||
|
self._file,
|
||||||
|
arg,
|
||||||
|
nsmap_scope=nsmap_scope,
|
||||||
|
global_nsmap=self.global_nsmap,
|
||||||
|
short_empty_elements=True,
|
||||||
|
is_html=self.is_html,
|
||||||
|
is_root=is_root,
|
||||||
|
uri_to_prefix=uri_to_prefix,
|
||||||
|
default_ns_attr_prefix=default_ns_attr_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
# without root the xml document is incomplete
|
||||||
|
if not self._have_root:
|
||||||
|
raise LxmlSyntaxError()
|
||||||
|
|
||||||
|
|
||||||
|
class xmlfile(object):
|
||||||
|
"""Context manager that can replace lxml.etree.xmlfile."""
|
||||||
|
def __init__(self, output_file, buffered=False, encoding="utf-8", close=False):
|
||||||
|
self._file = output_file
|
||||||
|
self._close = close
|
||||||
|
self.encoding = encoding
|
||||||
|
self.writer_cm = None
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.writer_cm = incremental_tree._get_writer(self._file, encoding=self.encoding)
|
||||||
|
writer, declared_encoding = self.writer_cm.__enter__()
|
||||||
|
return _IncrementalFileWriter(writer)
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
if self.writer_cm:
|
||||||
|
self.writer_cm.__exit__(type, value, traceback)
|
||||||
|
if self._close:
|
||||||
|
self._file.close()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
This software is under the MIT Licence
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Copyright (c) 2010 openpyxl
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: openpyxl
|
||||||
|
Version: 3.1.5
|
||||||
|
Summary: A Python library to read/write Excel 2010 xlsx/xlsm files
|
||||||
|
Home-page: https://openpyxl.readthedocs.io
|
||||||
|
Author: See AUTHORS
|
||||||
|
Author-email: charlie.clark@clark-consulting.eu
|
||||||
|
License: MIT
|
||||||
|
Project-URL: Documentation, https://openpyxl.readthedocs.io/en/stable/
|
||||||
|
Project-URL: Source, https://foss.heptapod.net/openpyxl/openpyxl
|
||||||
|
Project-URL: Tracker, https://foss.heptapod.net/openpyxl/openpyxl/-/issues
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Operating System :: MacOS :: MacOS X
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Operating System :: POSIX
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: 3.9
|
||||||
|
Classifier: Programming Language :: Python :: 3.10
|
||||||
|
Classifier: Programming Language :: Python :: 3.11
|
||||||
|
Requires-Python: >=3.8
|
||||||
|
License-File: LICENCE.rst
|
||||||
|
Requires-Dist: et-xmlfile
|
||||||
|
|
||||||
|
.. image:: https://coveralls.io/repos/bitbucket/openpyxl/openpyxl/badge.svg?branch=default
|
||||||
|
:target: https://coveralls.io/bitbucket/openpyxl/openpyxl?branch=default
|
||||||
|
:alt: coverage status
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
openpyxl is a Python library to read/write Excel 2010 xlsx/xlsm/xltx/xltm files.
|
||||||
|
|
||||||
|
It was born from lack of existing library to read/write natively from Python
|
||||||
|
the Office Open XML format.
|
||||||
|
|
||||||
|
All kudos to the PHPExcel team as openpyxl was initially based on PHPExcel.
|
||||||
|
|
||||||
|
|
||||||
|
Security
|
||||||
|
--------
|
||||||
|
|
||||||
|
By default openpyxl does not guard against quadratic blowup or billion laughs
|
||||||
|
xml attacks. To guard against these attacks install defusedxml.
|
||||||
|
|
||||||
|
Mailing List
|
||||||
|
------------
|
||||||
|
|
||||||
|
The user list can be found on http://groups.google.com/group/openpyxl-users
|
||||||
|
|
||||||
|
|
||||||
|
Sample code::
|
||||||
|
|
||||||
|
from openpyxl import Workbook
|
||||||
|
wb = Workbook()
|
||||||
|
|
||||||
|
# grab the active worksheet
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
# Data can be assigned directly to cells
|
||||||
|
ws['A1'] = 42
|
||||||
|
|
||||||
|
# Rows can also be appended
|
||||||
|
ws.append([1, 2, 3])
|
||||||
|
|
||||||
|
# Python types will automatically be converted
|
||||||
|
import datetime
|
||||||
|
ws['A2'] = datetime.datetime.now()
|
||||||
|
|
||||||
|
# Save the file
|
||||||
|
wb.save("sample.xlsx")
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The documentation is at: https://openpyxl.readthedocs.io
|
||||||
|
|
||||||
|
* installation methods
|
||||||
|
* code examples
|
||||||
|
* instructions for contributing
|
||||||
|
|
||||||
|
Release notes: https://openpyxl.readthedocs.io/en/stable/changes.html
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
openpyxl-3.1.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
openpyxl-3.1.5.dist-info/LICENCE.rst,sha256=DIS7QvXTZ-Xr-fwt3jWxYUHfXuD9wYklCFi8bFVg9p4,1131
|
||||||
|
openpyxl-3.1.5.dist-info/METADATA,sha256=I_gMqYMN2JQ12hcQ8m3tqPgeVAkofnRUAhDHJiekrZY,2510
|
||||||
|
openpyxl-3.1.5.dist-info/RECORD,,
|
||||||
|
openpyxl-3.1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
openpyxl-3.1.5.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
||||||
|
openpyxl-3.1.5.dist-info/top_level.txt,sha256=mKJO5QFAsUEDtJ_c97F-IbmVtHYEDymqD7d5X0ULkVs,9
|
||||||
|
openpyxl/__init__.py,sha256=s2sXcp8ThXXHswNSh-UuQi5BHsoasuczUyjNNz0Vupc,603
|
||||||
|
openpyxl/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/__pycache__/_constants.cpython-312.pyc,,
|
||||||
|
openpyxl/_constants.py,sha256=rhOeQ6wNH6jw73G4I242VtbmyM8fvdNVwOsOjJlJ6TU,306
|
||||||
|
openpyxl/cell/__init__.py,sha256=OXNzFFR9dlxUXiuWXyKSVQRJiQhZFel-_RQS3mHNnrQ,122
|
||||||
|
openpyxl/cell/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/cell/__pycache__/_writer.cpython-312.pyc,,
|
||||||
|
openpyxl/cell/__pycache__/cell.cpython-312.pyc,,
|
||||||
|
openpyxl/cell/__pycache__/read_only.cpython-312.pyc,,
|
||||||
|
openpyxl/cell/__pycache__/rich_text.cpython-312.pyc,,
|
||||||
|
openpyxl/cell/__pycache__/text.cpython-312.pyc,,
|
||||||
|
openpyxl/cell/_writer.py,sha256=3I6WLKEJGuFe8rOjxdAVuDT4sZYjcYo57-6velGepdQ,4015
|
||||||
|
openpyxl/cell/cell.py,sha256=hVJsMC9kJAxxb_CspJlBrwDt2qzfccO6YDfPHK3BBCQ,8922
|
||||||
|
openpyxl/cell/read_only.py,sha256=ApXkofmUK5QISsuTgZvmZKsU8PufSQtqe2xmYWTgLnc,3097
|
||||||
|
openpyxl/cell/rich_text.py,sha256=uAZmGB7bYDUnanHI0vJmKbfSF8riuIYS5CwlVU_3_fM,5628
|
||||||
|
openpyxl/cell/text.py,sha256=acU6BZQNSmVx4bBXPgFavoxmfoPbVYrm_ztp1bGeOmc,4367
|
||||||
|
openpyxl/chart/_3d.py,sha256=Sdm0TNpXHXNoOLUwiOSccv7yFwrel_-rjQhkrDqAAF4,3104
|
||||||
|
openpyxl/chart/__init__.py,sha256=ag4YCN1B3JH0lkS7tiiZCohVAA51x_pejGdAMuxaI1Y,564
|
||||||
|
openpyxl/chart/__pycache__/_3d.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/area_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/axis.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/bar_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/bubble_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/chartspace.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/data_source.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/descriptors.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/error_bar.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/label.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/layout.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/legend.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/line_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/marker.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/picture.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/pie_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/pivot.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/plotarea.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/print_settings.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/radar_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/reader.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/reference.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/scatter_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/series.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/series_factory.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/shapes.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/stock_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/surface_chart.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/text.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/title.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/trendline.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/__pycache__/updown_bars.cpython-312.pyc,,
|
||||||
|
openpyxl/chart/_chart.py,sha256=j5xn6mQYmZ4E7y2V1Xvx1jwhX2_O68Mp-8zeXRteS7E,5746
|
||||||
|
openpyxl/chart/area_chart.py,sha256=uROD3fdus6yD1TGu87j4z7KtOEH7tI-3Z5NFK73wwgw,2890
|
||||||
|
openpyxl/chart/axis.py,sha256=yommy5q2mQWKmmLRouWBpimiBZDBM1K-UKAIwCwKDNc,12580
|
||||||
|
openpyxl/chart/bar_chart.py,sha256=_TQHleMT3gSa6B1BkKD_FkLFcv8LRaoiHbpy2yflLO4,4142
|
||||||
|
openpyxl/chart/bubble_chart.py,sha256=KL7VZYFyLDpA8MC-IFtRAUIN262xK6MzjU41DrSVgpY,2004
|
||||||
|
openpyxl/chart/chartspace.py,sha256=PuPGBsVbpK5JagbB7SWgp4JwdQtTrZzIm8mf3kfGAuY,6069
|
||||||
|
openpyxl/chart/data_source.py,sha256=GAuWoCOJ4k7RZNJZkZck0zt_-D5UfDEwqwQ3ND4-s34,5782
|
||||||
|
openpyxl/chart/descriptors.py,sha256=uj-qptwKOBeg7U5xBN4QJQ2OwQvFQ7o4n5eMXXIWS7M,736
|
||||||
|
openpyxl/chart/error_bar.py,sha256=GS_L7PiyKNnJVHvQqG2hLxEW237igLLCatCNC-xGMxk,1832
|
||||||
|
openpyxl/chart/label.py,sha256=IjvI-CZjTY8ydoUzUOihcbxoRWiSpFb_ipD6C2I8Pu4,4133
|
||||||
|
openpyxl/chart/layout.py,sha256=QHakp_CIcoNuvjyZMsQ2p_qP44DIQs4aquy7yln94JM,2040
|
||||||
|
openpyxl/chart/legend.py,sha256=iPMycOhYDAVYd05OU_QDB-GSavdw_1L9CMuJIETOoGI,2040
|
||||||
|
openpyxl/chart/line_chart.py,sha256=6tAyDCzFiuiBFuUDTWhQepH8xVCx2s57lH951cEcwn0,3951
|
||||||
|
openpyxl/chart/marker.py,sha256=kfybMkshK3qefOUW7OX-Os0vfl5OCXfg8MytwHC2i-w,2600
|
||||||
|
openpyxl/chart/picture.py,sha256=Q4eBNQMKQDHR91RnPc7tM-YZVdcnWncedUlfagj67gk,1156
|
||||||
|
openpyxl/chart/pie_chart.py,sha256=UOvkjrBpNd_rT-rvKcpPeVd9dK-ELdMIaHjAUEr6oN8,4793
|
||||||
|
openpyxl/chart/pivot.py,sha256=9kVDmnxnR0uQRQ-Wbl6qw8eew9LGhqomaDBaXqQGZY4,1741
|
||||||
|
openpyxl/chart/plotarea.py,sha256=em7yorXFz9SmJruqOR4Pn-2oEj0Su4rnzyNc5e0IZ_U,5805
|
||||||
|
openpyxl/chart/print_settings.py,sha256=UwB6Kn6xkLRBejXScl-utF8dkNhV7Lm3Lfk7ACpbRgs,1454
|
||||||
|
openpyxl/chart/radar_chart.py,sha256=93I1Y1dmXZ6Y0F1VKXz9I3x1ufgwygBOdbPZumR5n3s,1521
|
||||||
|
openpyxl/chart/reader.py,sha256=oQD-29oxSLW2yzXdyXNhzQYNXgM64Y3kVSOIkrPZCuU,802
|
||||||
|
openpyxl/chart/reference.py,sha256=N3T4qYMH9BVrtbDRiKIZz-qGvPAdfquWTGL0XKxD9G8,3098
|
||||||
|
openpyxl/chart/scatter_chart.py,sha256=JMU32jjxTj7txPJ2TebBHPS5UcMsRHVqLz_psnN2YZs,1563
|
||||||
|
openpyxl/chart/series.py,sha256=k8eR8cviH9EPllRjjr_2a-lH5S3_HWBTLyE7XKghzWc,5896
|
||||||
|
openpyxl/chart/series_factory.py,sha256=ey1zgNwM1g4bQwB9lLhM6E-ctLIM2kLWM3X7CPw8SDs,1368
|
||||||
|
openpyxl/chart/shapes.py,sha256=JkgMy3DUWDKLV6JZHKb_pUBvWpzTAQ3biUMr-1fJWZU,2815
|
||||||
|
openpyxl/chart/stock_chart.py,sha256=YJ7eElBX5omHziKo41ygTA7F_NEkyIlFUfdDJXZuKhM,1604
|
||||||
|
openpyxl/chart/surface_chart.py,sha256=_-yGEX-Ou2NJVmJCA_K_bSLyzk-RvbPupyQLmjfCWj0,2914
|
||||||
|
openpyxl/chart/text.py,sha256=voJCf4PK5olmX0g_5u9aQo8B5LpCUlOeq4j4pnOy_A0,1847
|
||||||
|
openpyxl/chart/title.py,sha256=L-7KxwcpMb2aZk4ikgMsIgFPVtBafIppx9ykd5FPJ4w,1952
|
||||||
|
openpyxl/chart/trendline.py,sha256=9pWSJa9Adwtd6v_i7dPT7qNKzhOrSMWZ4QuAOntZWVg,3045
|
||||||
|
openpyxl/chart/updown_bars.py,sha256=QA4lyEMtMVvZCrYUpHZYMVS1xsnaN4_T5UBi6E7ilQ0,897
|
||||||
|
openpyxl/chartsheet/__init__.py,sha256=3Ony1WNbxxWuddTW-peuUPvO3xqIWFWe3Da2OUzsVnI,71
|
||||||
|
openpyxl/chartsheet/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/__pycache__/chartsheet.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/__pycache__/custom.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/__pycache__/properties.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/__pycache__/protection.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/__pycache__/publish.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/__pycache__/relation.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/__pycache__/views.cpython-312.pyc,,
|
||||||
|
openpyxl/chartsheet/chartsheet.py,sha256=GTXNfQPYBaS4B7XB4f7gDkAo2kCjtZqidl6iDxp-JQ8,3911
|
||||||
|
openpyxl/chartsheet/custom.py,sha256=qVgeCzT7t1tN_pDwaLqtR3ubuPDLeTR5KKlcxwnTWa8,1691
|
||||||
|
openpyxl/chartsheet/properties.py,sha256=dR1nrp22FsPkyDrwQaZV7t-p-Z2Jc88Y2IhIGbBvFhk,679
|
||||||
|
openpyxl/chartsheet/protection.py,sha256=eJixEBmdoTDO2_0h6g51sdSdfSdCaP8UUNsbEqHds6U,1265
|
||||||
|
openpyxl/chartsheet/publish.py,sha256=PrwqsUKn2SK67ZM3NEGT9FH4nOKC1cOxxm3322hHawQ,1587
|
||||||
|
openpyxl/chartsheet/relation.py,sha256=ZAAfEZb639ve0k6ByRwmHdjBrjqVC0bHOLgIcBwRx6o,2731
|
||||||
|
openpyxl/chartsheet/views.py,sha256=My3Au-DEAcC4lwBARhrCcwsN7Lp9H6cFQT-SiAcJlko,1341
|
||||||
|
openpyxl/comments/__init__.py,sha256=k_QJ-OPRme8HgAYQlyxbbRhmS1n2FyowqIeekBW-7vw,67
|
||||||
|
openpyxl/comments/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/comments/__pycache__/author.cpython-312.pyc,,
|
||||||
|
openpyxl/comments/__pycache__/comment_sheet.cpython-312.pyc,,
|
||||||
|
openpyxl/comments/__pycache__/comments.cpython-312.pyc,,
|
||||||
|
openpyxl/comments/__pycache__/shape_writer.cpython-312.pyc,,
|
||||||
|
openpyxl/comments/author.py,sha256=PZB_fjQqiEm8BdHDblbfzB0gzkFvECWq5i1jSHeJZco,388
|
||||||
|
openpyxl/comments/comment_sheet.py,sha256=Uv2RPpIxrikDPHBr5Yj1dDkusZB97yVE-NQTM0-EnBk,5753
|
||||||
|
openpyxl/comments/comments.py,sha256=CxurAWM7WbCdbeya-DQklbiWSFaxhtrUNBZEzulTyxc,1466
|
||||||
|
openpyxl/comments/shape_writer.py,sha256=Ls1d0SscfxGM9H2spjxMNHeJSaZJuLawlXs4t4qH7v4,3809
|
||||||
|
openpyxl/compat/__init__.py,sha256=fltF__CdGK97l2V3MtIDxbwgV_p1AZvLdyqcEtXKsqs,1592
|
||||||
|
openpyxl/compat/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/compat/__pycache__/abc.cpython-312.pyc,,
|
||||||
|
openpyxl/compat/__pycache__/numbers.cpython-312.pyc,,
|
||||||
|
openpyxl/compat/__pycache__/product.cpython-312.pyc,,
|
||||||
|
openpyxl/compat/__pycache__/singleton.cpython-312.pyc,,
|
||||||
|
openpyxl/compat/__pycache__/strings.cpython-312.pyc,,
|
||||||
|
openpyxl/compat/abc.py,sha256=Y-L6pozzgjr81OfXsjDkGDeKEq6BOfMr6nvrFps_o6Q,155
|
||||||
|
openpyxl/compat/numbers.py,sha256=2dckE0PHT7eB89Sc2BdlWOH4ZLXWt3_eo73-CzRujUY,1617
|
||||||
|
openpyxl/compat/product.py,sha256=-bDgNMHGDgbahgw0jqale8TeIARLw7HO0soQAL9b_4k,264
|
||||||
|
openpyxl/compat/singleton.py,sha256=R1HiH7XpjaW4kr3GILWMc4hRGZkXyc0yK7T1jcg_QWg,1023
|
||||||
|
openpyxl/compat/strings.py,sha256=D_TWf8QnMH6WMx6xuCDfXl0boc1k9q7j8hGalVQ2RUk,604
|
||||||
|
openpyxl/descriptors/__init__.py,sha256=eISTR0Sa1ZKKNQPxMZtqlE39JugYzkjxiZf7u9fttiw,1952
|
||||||
|
openpyxl/descriptors/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/base.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/container.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/excel.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/namespace.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/nested.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/sequence.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/serialisable.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/__pycache__/slots.cpython-312.pyc,,
|
||||||
|
openpyxl/descriptors/base.py,sha256=-CuNfswEGazgOoX3GuM2Bs2zkBImT992TvR2R1xsnXM,7135
|
||||||
|
openpyxl/descriptors/container.py,sha256=IcO91M02hR0vXZtWGurz0IH1Vi2PoEECP1PEbz62FJQ,889
|
||||||
|
openpyxl/descriptors/excel.py,sha256=d6a6mtoZ-33jwMGlgvNTL54cqLANKyhMihG6887j8r0,2412
|
||||||
|
openpyxl/descriptors/namespace.py,sha256=LjI4e9R09NSbClr_ewv0YmHgWY8RO5xq1s-SpAvz2wo,313
|
||||||
|
openpyxl/descriptors/nested.py,sha256=5LSsf2uvTKsrGEEQF1KVXMLHZFoRgmLfL_lzW0lWQjI,2603
|
||||||
|
openpyxl/descriptors/sequence.py,sha256=OqF34K_nUC46XD5B_6xzGHeEICz_82hkFkNFXpBkSSE,3490
|
||||||
|
openpyxl/descriptors/serialisable.py,sha256=U_7wMEGQRIOiimUUL4AbdOiWMc_aLyKeaRnj_Z7dVO8,7361
|
||||||
|
openpyxl/descriptors/slots.py,sha256=xNj5vLWWoounpYqbP2JDnnhlTiTLRn-uTfQxncpFfn0,824
|
||||||
|
openpyxl/drawing/__init__.py,sha256=xlXVaT3Fs9ltvbbRIGTSRow9kw9nhLY3Zj1Mm6vXRHE,66
|
||||||
|
openpyxl/drawing/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/colors.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/connector.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/drawing.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/effect.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/fill.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/geometry.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/graphic.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/image.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/line.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/picture.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/properties.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/relation.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/spreadsheet_drawing.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/text.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/__pycache__/xdr.cpython-312.pyc,,
|
||||||
|
openpyxl/drawing/colors.py,sha256=d92d6LQv2xi4xVt0F6bEJz-kpe4ahghNsOIY0_cxgQI,15251
|
||||||
|
openpyxl/drawing/connector.py,sha256=4be6kFwDmixqYX6ko22JE3cqJ9xUM7lRonSer1BDVgY,3863
|
||||||
|
openpyxl/drawing/drawing.py,sha256=Wbv24TZbNaPngDR3adOj6jUBg-iyMYyfvgEPg-5IPu8,2339
|
||||||
|
openpyxl/drawing/effect.py,sha256=vZ5r9k3JfyaAoBggFzN9wyvsEDnMnAmkQZsdVQN1-wo,9435
|
||||||
|
openpyxl/drawing/fill.py,sha256=Z_kAY5bncgu1WkZNvgjiX5ucrYI6GLXyUi6H3_mne2k,13092
|
||||||
|
openpyxl/drawing/geometry.py,sha256=0UM5hMHYy_R3C-lHt5x3NECDn7O1tfbKu5BweLwdLlg,17523
|
||||||
|
openpyxl/drawing/graphic.py,sha256=013KhmTqp1PFKht9lRRA6SHjznxq9EL4u_ybA88OuCk,4811
|
||||||
|
openpyxl/drawing/image.py,sha256=ROO0YJjzH9eqjPUKU5bMtt4bXnHFK9uofDa2__R3G2k,1455
|
||||||
|
openpyxl/drawing/line.py,sha256=CRxV0NUpce4RfXPDllodcneoHk8vr2Ind8HaWnUv2HE,3904
|
||||||
|
openpyxl/drawing/picture.py,sha256=tDYob2x4encQ9rUWOe29PqtiRSDEj746j-SvQ7rVV10,4205
|
||||||
|
openpyxl/drawing/properties.py,sha256=TyLOF3ehp38XJvuupNZdsOqZ0HNXkVPBDYwU5O1GhBM,4948
|
||||||
|
openpyxl/drawing/relation.py,sha256=InbM75ymWUjICXhjyCcYqp1FWcfCFp9q9vecYLptzk4,344
|
||||||
|
openpyxl/drawing/spreadsheet_drawing.py,sha256=CUWSpIYWOHUEp-USOAGVNlLfXBQObcGdg_RZ_bggPYM,10721
|
||||||
|
openpyxl/drawing/text.py,sha256=6_ShIu9FLG7MJvMLs_G_tTatTaBqxpaX5KMKxSfTY7Y,22421
|
||||||
|
openpyxl/drawing/xdr.py,sha256=XE2yRzlCqoJBWg3TPRxelzZ4GmBV9dDFTtiJgJZku-U,626
|
||||||
|
openpyxl/formatting/__init__.py,sha256=vpkL3EimMa-moJjcWk4l3bIWdJ3c7a8pKOfGlnPte9c,59
|
||||||
|
openpyxl/formatting/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/formatting/__pycache__/formatting.cpython-312.pyc,,
|
||||||
|
openpyxl/formatting/__pycache__/rule.cpython-312.pyc,,
|
||||||
|
openpyxl/formatting/formatting.py,sha256=AdXlrhic4CPvyJ300oFJPJUH-2vS0VNOLiNudt3U26c,2701
|
||||||
|
openpyxl/formatting/rule.py,sha256=96Fc5-hSByCrvkC1O0agEoZyL9G_AdeulrjRXnf_rZ8,9288
|
||||||
|
openpyxl/formula/__init__.py,sha256=AgvEdunVryhzwecuFVO2EezdJT3h5gCXpw2j3f5VUWA,69
|
||||||
|
openpyxl/formula/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/formula/__pycache__/tokenizer.cpython-312.pyc,,
|
||||||
|
openpyxl/formula/__pycache__/translate.cpython-312.pyc,,
|
||||||
|
openpyxl/formula/tokenizer.py,sha256=o1jDAOl79YiCWr-2LmSICyAbhm2hdb-37jriasmv4dc,15088
|
||||||
|
openpyxl/formula/translate.py,sha256=Zs9adqfZTAuo8J_QNbqK3vjQDlSFhWc0vWc6TCMDYrI,6653
|
||||||
|
openpyxl/packaging/__init__.py,sha256=KcNtO2zoYizOgG-iZzayZffSL1WeZR98i1Q8QYTRhfI,90
|
||||||
|
openpyxl/packaging/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/__pycache__/core.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/__pycache__/custom.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/__pycache__/extended.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/__pycache__/interface.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/__pycache__/manifest.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/__pycache__/relationship.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/__pycache__/workbook.cpython-312.pyc,,
|
||||||
|
openpyxl/packaging/core.py,sha256=OSbSFGZrKYcZszcHe3LhQEyiAf2Wylwxm4_6N8WO-Yo,4061
|
||||||
|
openpyxl/packaging/custom.py,sha256=uCEl7IwITFX2pOxiAITnvNbfsav80uHB0wXUFvjIRUQ,6738
|
||||||
|
openpyxl/packaging/extended.py,sha256=JFksxDd67rA57n-vxg48tbeZh2g2LEOb0fgJLeqbTWM,4810
|
||||||
|
openpyxl/packaging/interface.py,sha256=vlGVt4YvyUR4UX9Tr9xmkn1G8s_ynYVtAx4okJ6-g_8,920
|
||||||
|
openpyxl/packaging/manifest.py,sha256=y5zoDQnhJ1aW_HPLItY_WE94fSLS4jxvfIqn_J2zJ6Q,5366
|
||||||
|
openpyxl/packaging/relationship.py,sha256=jLhvFvDVZBRTZTXokRrrsEiLI9CmFlulhGzA_OYKM0Q,3974
|
||||||
|
openpyxl/packaging/workbook.py,sha256=s4jl4gqqMkaUHmMAR52dc9ZoNTieuXcq1OG3cgNDYjw,6495
|
||||||
|
openpyxl/pivot/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
|
||||||
|
openpyxl/pivot/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/pivot/__pycache__/cache.cpython-312.pyc,,
|
||||||
|
openpyxl/pivot/__pycache__/fields.cpython-312.pyc,,
|
||||||
|
openpyxl/pivot/__pycache__/record.cpython-312.pyc,,
|
||||||
|
openpyxl/pivot/__pycache__/table.cpython-312.pyc,,
|
||||||
|
openpyxl/pivot/cache.py,sha256=kKQMEcoYb9scl_CNNWfmNOTewD5S3hpBGwViMtDCyx0,27840
|
||||||
|
openpyxl/pivot/fields.py,sha256=0CQLdTOBhYAa9gfEZb_bvkgCx8feASYp64dqFskDkqU,7057
|
||||||
|
openpyxl/pivot/record.py,sha256=c45ft1YsPAVRneMVh_WvUQ1nZt9RJQ_josRuolKx3qE,2671
|
||||||
|
openpyxl/pivot/table.py,sha256=riKBeb1aICXWipnhpSaSx9iqP-AkfcyOSm3Dfl407dA,40756
|
||||||
|
openpyxl/reader/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
|
||||||
|
openpyxl/reader/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/reader/__pycache__/drawings.cpython-312.pyc,,
|
||||||
|
openpyxl/reader/__pycache__/excel.cpython-312.pyc,,
|
||||||
|
openpyxl/reader/__pycache__/strings.cpython-312.pyc,,
|
||||||
|
openpyxl/reader/__pycache__/workbook.cpython-312.pyc,,
|
||||||
|
openpyxl/reader/drawings.py,sha256=iZPok8Dc_mZMyRPk_EfDXDQvZdwfHwbYjvxfK2cXtag,2209
|
||||||
|
openpyxl/reader/excel.py,sha256=kgStQtO1j0vV56GWaXxo3GA2EXuouGtnFrRVMocq8EY,12357
|
||||||
|
openpyxl/reader/strings.py,sha256=oG2Mq6eBD0-ElFOxPdHTBUmshUxTNrK1sns1UJRaVis,1113
|
||||||
|
openpyxl/reader/workbook.py,sha256=4w0LRV7qNNGHDnYd19zUgWnJOEX8tHjm3vlkxwllzv4,4352
|
||||||
|
openpyxl/styles/__init__.py,sha256=2QNNdlz4CjhnkBQVNhZ-12Yz73_uHIinqRKWo_TjNwg,363
|
||||||
|
openpyxl/styles/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/alignment.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/borders.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/builtins.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/cell_style.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/colors.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/differential.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/fills.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/fonts.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/named_styles.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/numbers.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/protection.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/proxy.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/styleable.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/stylesheet.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/__pycache__/table.cpython-312.pyc,,
|
||||||
|
openpyxl/styles/alignment.py,sha256=wQOEtmYhPJFtnuBq0juMe5EsCp9DNSVS1ieBhlAnwWE,2198
|
||||||
|
openpyxl/styles/borders.py,sha256=BLUTOyBbxWQzv8Kuh1u4sWfJiIPJc8QExb7nGwdSmXc,3302
|
||||||
|
openpyxl/styles/builtins.py,sha256=cMtJverVSjdIdCckP6L-AlI0OLMRPgbQwaJWUkldA0U,31182
|
||||||
|
openpyxl/styles/cell_style.py,sha256=8Ol5F6ktKeSqhDVF-10w5eIh7W-jkzijpPPHqqv1qDs,5414
|
||||||
|
openpyxl/styles/colors.py,sha256=Ss3QqNS5YISVkJxlNfd4q_YSrFKdKjATWLDSu2rPMBc,4612
|
||||||
|
openpyxl/styles/differential.py,sha256=dqEGny_ou1jC3tegBal1w_UbONyQEJXvGPURs8xWwfU,2267
|
||||||
|
openpyxl/styles/fills.py,sha256=LmR4H00GzKDWyYjzDEayzKZN28S_muD65DvAFWlbaCI,6380
|
||||||
|
openpyxl/styles/fonts.py,sha256=nkeiJUgKYnWaETvn51sOo9zQXJiOEJKHDTqvxt0JiBc,3516
|
||||||
|
openpyxl/styles/named_styles.py,sha256=nfL1KPpd6b0Y0qBrGJQ15EUOebfeO1eZBQhPVpcZW-o,7254
|
||||||
|
openpyxl/styles/numbers.py,sha256=6kK7mdBD-0xs7bjYDFNGsUAvoFvRu5wSMjOF9J5j-Go,5097
|
||||||
|
openpyxl/styles/protection.py,sha256=BUHgARq7SjOVfW_ST53hKCUofVBEWXn3Lnn_c5n4i_I,394
|
||||||
|
openpyxl/styles/proxy.py,sha256=ajsvzRp_MOeV_rZSEfVoti6-3tW8aowo5_Hjwp2AlfA,1432
|
||||||
|
openpyxl/styles/styleable.py,sha256=Yl_-oPljEuFzg9tXKSSCuvWRL4L0HC5bHMFJVhex6Oc,4499
|
||||||
|
openpyxl/styles/stylesheet.py,sha256=7kZpzyavLrOJcdZqZzl3WZTyM60CqWP8i_OQ0J_1xy0,8790
|
||||||
|
openpyxl/styles/table.py,sha256=VexRqPPQmjRzWe1rVTOgyOQgvlCBuEYTif5MEV_0qsk,2801
|
||||||
|
openpyxl/utils/__init__.py,sha256=wCMNXgIoA4aF4tpSuSzxm1k3SmJJGOEjtdbqdJZZG7I,324
|
||||||
|
openpyxl/utils/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/bound_dictionary.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/cell.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/dataframe.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/datetime.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/escape.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/exceptions.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/formulas.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/indexed_list.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/inference.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/protection.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/__pycache__/units.cpython-312.pyc,,
|
||||||
|
openpyxl/utils/bound_dictionary.py,sha256=zfzflQom1FqfEw8uexBqI8eExCeAWELzSk4TqqpD-w8,717
|
||||||
|
openpyxl/utils/cell.py,sha256=P7og4c4JcSN__amIsubIMgSMlQ4SrAA5eZ0cjkoXlaQ,6967
|
||||||
|
openpyxl/utils/dataframe.py,sha256=d3SPeb4p9YKFwlFTUWhdVUYYyMLNrd9atC6iSf2QB6w,2957
|
||||||
|
openpyxl/utils/datetime.py,sha256=xQ8zHJFb-n4nlN6fA_fFZKHlHeNOB7El48p9-YOPvGE,4529
|
||||||
|
openpyxl/utils/escape.py,sha256=4dgcSlSdPNk0vkJNHRUK9poEe8pn4sBIQ5Rjz-7H1Uk,790
|
||||||
|
openpyxl/utils/exceptions.py,sha256=WT40gTyd9YUhg1MeqZNzHp9qJnL5eXzbCEb_VtHp3Kk,889
|
||||||
|
openpyxl/utils/formulas.py,sha256=-I0zyvicBZMaAH1XzsmmEEzE4GB-NA605aArWVt9ik4,4248
|
||||||
|
openpyxl/utils/indexed_list.py,sha256=hBsQP9gunTit7iKdMGw_tM3y5uIpXDjUx7jswbQF6Dc,1257
|
||||||
|
openpyxl/utils/inference.py,sha256=dM1FBW_Rx_xE7P8vGo6WNhbBe-2eqpGuJj4eqdS7UjE,1583
|
||||||
|
openpyxl/utils/protection.py,sha256=opm7GVM2ePQvpNzKT-W56u-0yP8liS9WJkxpzpG_tE0,830
|
||||||
|
openpyxl/utils/units.py,sha256=eGpGrdzyoKlqLs99eALNC5c1gSLXRo4GdUNAqdB4wzg,2642
|
||||||
|
openpyxl/workbook/__init__.py,sha256=yKMikN8VqoVZJcoZSVW3p9Smt88ibeqNq9NHhGBJqEM,68
|
||||||
|
openpyxl/workbook/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/_writer.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/child.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/defined_name.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/external_reference.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/function_group.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/properties.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/protection.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/smart_tags.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/views.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/web.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/__pycache__/workbook.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/_writer.py,sha256=pB4s05erNEBJFT_w5LT-2DlxqXkZLOutXWVgewRLVds,6506
|
||||||
|
openpyxl/workbook/child.py,sha256=r_5V9DNkGSYZhzi62P10ZnsO5iT518YopcTdmSvtAUc,4052
|
||||||
|
openpyxl/workbook/defined_name.py,sha256=EAF1WvGYU4WG7dusDi29yBAr15BhkYtkF_GrFym1DDY,5394
|
||||||
|
openpyxl/workbook/external_link/__init__.py,sha256=YOkLI226nyopB6moShzGIfBRckdQgPiFXjVZwXW-DpE,71
|
||||||
|
openpyxl/workbook/external_link/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/external_link/__pycache__/external.cpython-312.pyc,,
|
||||||
|
openpyxl/workbook/external_link/external.py,sha256=LXXuej0-d0iRnwlJ-13S81kbuDxvhAWo3qfnxpsClvM,4509
|
||||||
|
openpyxl/workbook/external_reference.py,sha256=9bKX9_QgNJxv7fEUd0G-ocXyZajMAsDzG11d0miguxY,348
|
||||||
|
openpyxl/workbook/function_group.py,sha256=x5QfUpFdsjtbFbAJzZof7SrZ376nufNY92mpCcaSPiQ,803
|
||||||
|
openpyxl/workbook/properties.py,sha256=vMUriu67iQU11xIos37ayv73gjq1kdHgI27ncJ3Vk24,5261
|
||||||
|
openpyxl/workbook/protection.py,sha256=LhiyuoOchdrun9xMwq_pxGzbkysziThfKivk0dHHOLw,6008
|
||||||
|
openpyxl/workbook/smart_tags.py,sha256=xHHXCrUPnHeRoM_eakrCOz-eCpct3Y7xKHShr9wGv7s,1181
|
||||||
|
openpyxl/workbook/views.py,sha256=uwQqZCrRavAoBDLZIBtgz7riOEhEaHplybV4cX_TMgY,5214
|
||||||
|
openpyxl/workbook/web.py,sha256=87B5mEZ6vfHTwywcGtcYL6u7D3RyJVDCJxV0nHZeS-w,2642
|
||||||
|
openpyxl/workbook/workbook.py,sha256=oaErvSH1qUphUAPOZTCHj2UHyKeDqsj2DycKCDcgo7M,13232
|
||||||
|
openpyxl/worksheet/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
|
||||||
|
openpyxl/worksheet/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/_read_only.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/_reader.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/_write_only.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/_writer.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/cell_range.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/cell_watch.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/controls.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/copier.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/custom.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/datavalidation.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/dimensions.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/drawing.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/errors.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/filters.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/formula.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/header_footer.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/hyperlink.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/merge.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/ole.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/page.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/pagebreak.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/picture.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/print_settings.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/properties.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/protection.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/related.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/scenario.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/smart_tag.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/table.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/views.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/__pycache__/worksheet.cpython-312.pyc,,
|
||||||
|
openpyxl/worksheet/_read_only.py,sha256=6Kd4Q-73UoJDY66skRJy_ks-wCHNttlGhsDxvB99PuY,5709
|
||||||
|
openpyxl/worksheet/_reader.py,sha256=vp_D7w4DiADMdyNrYpQglrCVvVLT9_DsSZikOd--n2c,16375
|
||||||
|
openpyxl/worksheet/_write_only.py,sha256=yqW-DtBDDYTwGCBHRVIwkheSB7SSLO3xlw-RsXtPorE,4232
|
||||||
|
openpyxl/worksheet/_writer.py,sha256=bDtw6BV5tdztARQEkQPprExRr8hZVFkj0DyolqxVu2k,10283
|
||||||
|
openpyxl/worksheet/cell_range.py,sha256=YP8AUnqUFP5wOV_avMDFRSZ0Qi2p78RWFuwyyCua7m8,15013
|
||||||
|
openpyxl/worksheet/cell_watch.py,sha256=LdxGcTmXbZ4sxm6inasFgZPld1ijdL5_ODSUvvz13DU,608
|
||||||
|
openpyxl/worksheet/controls.py,sha256=FPLg4N94T-IL27NLg8Le_U4WYDT_6Aa25LDG_kiEDVA,2735
|
||||||
|
openpyxl/worksheet/copier.py,sha256=0Di1qSks0g7Jtgmpc_M20O-KPCW81Yr2myC5j458nyU,2319
|
||||||
|
openpyxl/worksheet/custom.py,sha256=CRlQ98GwqqKmEDkv8gPUCa0ApNM2Vz-BLs_-RMu3jLA,639
|
||||||
|
openpyxl/worksheet/datavalidation.py,sha256=m-O7NOoTDr_bAfxB9xEeY5QttFiuPtzs-IFAlF0j4FE,6131
|
||||||
|
openpyxl/worksheet/dimensions.py,sha256=HzM77FrYixiQDCugRT-C9ZpKq7GNFaGchxT73U4cisY,9102
|
||||||
|
openpyxl/worksheet/drawing.py,sha256=2nfrLyTX0kAizPIINF12KwDW9mRnaq8hs-NrSBcWpmE,275
|
||||||
|
openpyxl/worksheet/errors.py,sha256=KkFC4bnckvCp74XsVXA7JUCi4MIimEFu3uAddcQpjo0,2435
|
||||||
|
openpyxl/worksheet/filters.py,sha256=8eUj2LuP8Qbz1R1gkK1c6W_UKS8-__6XlFMVkunIua0,13854
|
||||||
|
openpyxl/worksheet/formula.py,sha256=5yuul6s1l-K_78KXHC6HrF_pLhxypoldh5jMg7zmlyY,1045
|
||||||
|
openpyxl/worksheet/header_footer.py,sha256=91F6NUDUEwrhgeWrxG9XtDPyPD03XAtGU_ONBpkAfUc,7886
|
||||||
|
openpyxl/worksheet/hyperlink.py,sha256=sXzPkkjl9BWNzCxwwEEaSS53J37jIXPmnnED-j8MIBo,1103
|
||||||
|
openpyxl/worksheet/merge.py,sha256=gNOIH6EJ8wVcJpibAv4CMc7UpD7_DrGvgaCSvG2im5A,4125
|
||||||
|
openpyxl/worksheet/ole.py,sha256=khVvqMt4GPc9Yr6whLDfkUo51euyLXfJe1p4zFee4no,3530
|
||||||
|
openpyxl/worksheet/page.py,sha256=4jeSRcDE0S2RPzIAmA3Bh-uXRyq0hnbO5h5pJdGHbbQ,4901
|
||||||
|
openpyxl/worksheet/pagebreak.py,sha256=XXFIMOY4VdPQCd86nGPghA6hOfLGK5G_KFuvjBNPRsw,1811
|
||||||
|
openpyxl/worksheet/picture.py,sha256=72TctCxzk2JU8uFfjiEbTBufEe5eQxIieSPBRhU6m1Q,185
|
||||||
|
openpyxl/worksheet/print_settings.py,sha256=k_g4fkrs9bfz-S-RIKIBGqzVgubufMdryWQ3ejXQoRI,5215
|
||||||
|
openpyxl/worksheet/properties.py,sha256=9iXTOVC8B9C-2pp_iU5l0r5Fjf3Uzv0SIOUKRrZ2hw4,3087
|
||||||
|
openpyxl/worksheet/protection.py,sha256=vj5M6WWC5xKiHeWS_tJqXxrlOJHJ7GpW2JdPw7r9jjE,3758
|
||||||
|
openpyxl/worksheet/related.py,sha256=ZLDpgcrW6DWl8vvh2sSVB_r1JyG8bC8EicCBKjfssTs,335
|
||||||
|
openpyxl/worksheet/scenario.py,sha256=VlJW4pi1OTy1cJ9m7ZxazIy8PSlo17BGpnUYixmNotQ,2401
|
||||||
|
openpyxl/worksheet/smart_tag.py,sha256=nLbt04IqeJllk7TmNS1eTNdb7On5jMf3llfyy3otDSk,1608
|
||||||
|
openpyxl/worksheet/table.py,sha256=gjt-jNP8dhVy8w5g-gMJpfHO-eV1EoxJy91yi-5HG64,11671
|
||||||
|
openpyxl/worksheet/views.py,sha256=DkZcptwpbpklHILSlvK-a2LmJ7BWb1wbDcz2JVl7404,4974
|
||||||
|
openpyxl/worksheet/worksheet.py,sha256=4JM5qjoJumtcqftHFkimtFEQrz7E2DBmXnkVo7R3WX8,27572
|
||||||
|
openpyxl/writer/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
|
||||||
|
openpyxl/writer/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/writer/__pycache__/excel.cpython-312.pyc,,
|
||||||
|
openpyxl/writer/__pycache__/theme.cpython-312.pyc,,
|
||||||
|
openpyxl/writer/excel.py,sha256=6ioXn3hSHHIUnkW2wCyBgPA4CncO6FXL5yGSAzsqp6Y,9572
|
||||||
|
openpyxl/writer/theme.py,sha256=5Hhq-0uP55sf_Zhw7i3M9azCfCjALQxoo7CV_9QPmTA,10320
|
||||||
|
openpyxl/xml/__init__.py,sha256=A5Kj0GWk5XI-zJxbAL5vIppV_AgEHLRveGu8RK5c7U0,1016
|
||||||
|
openpyxl/xml/__pycache__/__init__.cpython-312.pyc,,
|
||||||
|
openpyxl/xml/__pycache__/constants.cpython-312.pyc,,
|
||||||
|
openpyxl/xml/__pycache__/functions.cpython-312.pyc,,
|
||||||
|
openpyxl/xml/constants.py,sha256=HDNnhcj-WO9ayO4Mqwca3Au0ZTNfsDqWDtleREs_Wto,4833
|
||||||
|
openpyxl/xml/functions.py,sha256=jBtfa8__w4gBlEPGHLGCAtJiaNKPyihTLsfmigyq2_Q,2025
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.43.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py2-none-any
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
openpyxl
|
||||||
19
venv/lib/python3.12/site-packages/openpyxl/__init__.py
Normal file
19
venv/lib/python3.12/site-packages/openpyxl/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
from openpyxl.compat.numbers import NUMPY
|
||||||
|
from openpyxl.xml import DEFUSEDXML, LXML
|
||||||
|
from openpyxl.workbook import Workbook
|
||||||
|
from openpyxl.reader.excel import load_workbook as open
|
||||||
|
from openpyxl.reader.excel import load_workbook
|
||||||
|
import openpyxl._constants as constants
|
||||||
|
|
||||||
|
# Expose constants especially the version number
|
||||||
|
|
||||||
|
__author__ = constants.__author__
|
||||||
|
__author_email__ = constants.__author_email__
|
||||||
|
__license__ = constants.__license__
|
||||||
|
__maintainer_email__ = constants.__maintainer_email__
|
||||||
|
__url__ = constants.__url__
|
||||||
|
__version__ = constants.__version__
|
||||||
13
venv/lib/python3.12/site-packages/openpyxl/_constants.py
Normal file
13
venv/lib/python3.12/site-packages/openpyxl/_constants.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
"""
|
||||||
|
Package metadata
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "See AUTHORS"
|
||||||
|
__author_email__ = "charlie.clark@clark-consulting.eu"
|
||||||
|
__license__ = "MIT"
|
||||||
|
__maintainer_email__ = "openpyxl-users@googlegroups.com"
|
||||||
|
__url__ = "https://openpyxl.readthedocs.io"
|
||||||
|
__version__ = "3.1.5"
|
||||||
|
__python__ = "3.8"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from .cell import Cell, WriteOnlyCell, MergedCell
|
||||||
|
from .read_only import ReadOnlyCell
|
||||||
136
venv/lib/python3.12/site-packages/openpyxl/cell/_writer.py
Normal file
136
venv/lib/python3.12/site-packages/openpyxl/cell/_writer.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.compat import safe_string
|
||||||
|
from openpyxl.xml.functions import Element, SubElement, whitespace, XML_NS
|
||||||
|
from openpyxl import LXML
|
||||||
|
from openpyxl.utils.datetime import to_excel, to_ISO8601
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from openpyxl.worksheet.formula import DataTableFormula, ArrayFormula
|
||||||
|
from openpyxl.cell.rich_text import CellRichText
|
||||||
|
|
||||||
|
def _set_attributes(cell, styled=None):
|
||||||
|
"""
|
||||||
|
Set coordinate and datatype
|
||||||
|
"""
|
||||||
|
coordinate = cell.coordinate
|
||||||
|
attrs = {'r': coordinate}
|
||||||
|
if styled:
|
||||||
|
attrs['s'] = f"{cell.style_id}"
|
||||||
|
|
||||||
|
if cell.data_type == "s":
|
||||||
|
attrs['t'] = "inlineStr"
|
||||||
|
elif cell.data_type != 'f':
|
||||||
|
attrs['t'] = cell.data_type
|
||||||
|
|
||||||
|
value = cell._value
|
||||||
|
|
||||||
|
if cell.data_type == "d":
|
||||||
|
if hasattr(value, "tzinfo") and value.tzinfo is not None:
|
||||||
|
raise TypeError("Excel does not support timezones in datetimes. "
|
||||||
|
"The tzinfo in the datetime/time object must be set to None.")
|
||||||
|
|
||||||
|
if cell.parent.parent.iso_dates and not isinstance(value, timedelta):
|
||||||
|
value = to_ISO8601(value)
|
||||||
|
else:
|
||||||
|
attrs['t'] = "n"
|
||||||
|
value = to_excel(value, cell.parent.parent.epoch)
|
||||||
|
|
||||||
|
if cell.hyperlink:
|
||||||
|
cell.parent._hyperlinks.append(cell.hyperlink)
|
||||||
|
|
||||||
|
return value, attrs
|
||||||
|
|
||||||
|
|
||||||
|
def etree_write_cell(xf, worksheet, cell, styled=None):
|
||||||
|
|
||||||
|
value, attributes = _set_attributes(cell, styled)
|
||||||
|
|
||||||
|
el = Element("c", attributes)
|
||||||
|
if value is None or value == "":
|
||||||
|
xf.write(el)
|
||||||
|
return
|
||||||
|
|
||||||
|
if cell.data_type == 'f':
|
||||||
|
attrib = {}
|
||||||
|
|
||||||
|
if isinstance(value, ArrayFormula):
|
||||||
|
attrib = dict(value)
|
||||||
|
value = value.text
|
||||||
|
|
||||||
|
elif isinstance(value, DataTableFormula):
|
||||||
|
attrib = dict(value)
|
||||||
|
value = None
|
||||||
|
|
||||||
|
formula = SubElement(el, 'f', attrib)
|
||||||
|
if value is not None and not attrib.get('t') == "dataTable":
|
||||||
|
formula.text = value[1:]
|
||||||
|
value = None
|
||||||
|
|
||||||
|
if cell.data_type == 's':
|
||||||
|
if isinstance(value, CellRichText):
|
||||||
|
el.append(value.to_tree())
|
||||||
|
else:
|
||||||
|
inline_string = Element("is")
|
||||||
|
text = Element('t')
|
||||||
|
text.text = value
|
||||||
|
whitespace(text)
|
||||||
|
inline_string.append(text)
|
||||||
|
el.append(inline_string)
|
||||||
|
|
||||||
|
else:
|
||||||
|
cell_content = SubElement(el, 'v')
|
||||||
|
if value is not None:
|
||||||
|
cell_content.text = safe_string(value)
|
||||||
|
|
||||||
|
xf.write(el)
|
||||||
|
|
||||||
|
|
||||||
|
def lxml_write_cell(xf, worksheet, cell, styled=False):
|
||||||
|
value, attributes = _set_attributes(cell, styled)
|
||||||
|
|
||||||
|
if value == '' or value is None:
|
||||||
|
with xf.element("c", attributes):
|
||||||
|
return
|
||||||
|
|
||||||
|
with xf.element('c', attributes):
|
||||||
|
if cell.data_type == 'f':
|
||||||
|
attrib = {}
|
||||||
|
|
||||||
|
if isinstance(value, ArrayFormula):
|
||||||
|
attrib = dict(value)
|
||||||
|
value = value.text
|
||||||
|
|
||||||
|
elif isinstance(value, DataTableFormula):
|
||||||
|
attrib = dict(value)
|
||||||
|
value = None
|
||||||
|
|
||||||
|
with xf.element('f', attrib):
|
||||||
|
if value is not None and not attrib.get('t') == "dataTable":
|
||||||
|
xf.write(value[1:])
|
||||||
|
value = None
|
||||||
|
|
||||||
|
if cell.data_type == 's':
|
||||||
|
if isinstance(value, CellRichText):
|
||||||
|
el = value.to_tree()
|
||||||
|
xf.write(el)
|
||||||
|
else:
|
||||||
|
with xf.element("is"):
|
||||||
|
if isinstance(value, str):
|
||||||
|
attrs = {}
|
||||||
|
if value != value.strip():
|
||||||
|
attrs["{%s}space" % XML_NS] = "preserve"
|
||||||
|
el = Element("t", attrs) # lxml can't handle xml-ns
|
||||||
|
el.text = value
|
||||||
|
xf.write(el)
|
||||||
|
|
||||||
|
else:
|
||||||
|
with xf.element("v"):
|
||||||
|
if value is not None:
|
||||||
|
xf.write(safe_string(value))
|
||||||
|
|
||||||
|
|
||||||
|
if LXML:
|
||||||
|
write_cell = lxml_write_cell
|
||||||
|
else:
|
||||||
|
write_cell = etree_write_cell
|
||||||
332
venv/lib/python3.12/site-packages/openpyxl/cell/cell.py
Normal file
332
venv/lib/python3.12/site-packages/openpyxl/cell/cell.py
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
"""Manage individual cells in a spreadsheet.
|
||||||
|
|
||||||
|
The Cell class is required to know its value and type, display options,
|
||||||
|
and any other features of an Excel cell. Utilities for referencing
|
||||||
|
cells using Excel's 'A1' column/row nomenclature are also provided.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__docformat__ = "restructuredtext en"
|
||||||
|
|
||||||
|
# Python stdlib imports
|
||||||
|
from copy import copy
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
from openpyxl.compat import (
|
||||||
|
NUMERIC_TYPES,
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.utils.exceptions import IllegalCharacterError
|
||||||
|
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from openpyxl.styles import numbers, is_date_format
|
||||||
|
from openpyxl.styles.styleable import StyleableObject
|
||||||
|
from openpyxl.worksheet.hyperlink import Hyperlink
|
||||||
|
from openpyxl.worksheet.formula import DataTableFormula, ArrayFormula
|
||||||
|
from openpyxl.cell.rich_text import CellRichText
|
||||||
|
|
||||||
|
# constants
|
||||||
|
|
||||||
|
TIME_TYPES = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
|
||||||
|
TIME_FORMATS = {
|
||||||
|
datetime.datetime:numbers.FORMAT_DATE_DATETIME,
|
||||||
|
datetime.date:numbers.FORMAT_DATE_YYYYMMDD2,
|
||||||
|
datetime.time:numbers.FORMAT_DATE_TIME6,
|
||||||
|
datetime.timedelta:numbers.FORMAT_DATE_TIMEDELTA,
|
||||||
|
}
|
||||||
|
|
||||||
|
STRING_TYPES = (str, bytes, CellRichText)
|
||||||
|
KNOWN_TYPES = NUMERIC_TYPES + TIME_TYPES + STRING_TYPES + (bool, type(None))
|
||||||
|
|
||||||
|
ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]')
|
||||||
|
ERROR_CODES = ('#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!',
|
||||||
|
'#N/A')
|
||||||
|
|
||||||
|
TYPE_STRING = 's'
|
||||||
|
TYPE_FORMULA = 'f'
|
||||||
|
TYPE_NUMERIC = 'n'
|
||||||
|
TYPE_BOOL = 'b'
|
||||||
|
TYPE_NULL = 'n'
|
||||||
|
TYPE_INLINE = 'inlineStr'
|
||||||
|
TYPE_ERROR = 'e'
|
||||||
|
TYPE_FORMULA_CACHE_STRING = 'str'
|
||||||
|
|
||||||
|
VALID_TYPES = (TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL,
|
||||||
|
TYPE_NULL, TYPE_INLINE, TYPE_ERROR, TYPE_FORMULA_CACHE_STRING)
|
||||||
|
|
||||||
|
|
||||||
|
_TYPES = {int:'n', float:'n', str:'s', bool:'b'}
|
||||||
|
|
||||||
|
|
||||||
|
def get_type(t, value):
|
||||||
|
if isinstance(value, NUMERIC_TYPES):
|
||||||
|
dt = 'n'
|
||||||
|
elif isinstance(value, STRING_TYPES):
|
||||||
|
dt = 's'
|
||||||
|
elif isinstance(value, TIME_TYPES):
|
||||||
|
dt = 'd'
|
||||||
|
elif isinstance(value, (DataTableFormula, ArrayFormula)):
|
||||||
|
dt = 'f'
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
_TYPES[t] = dt
|
||||||
|
return dt
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_format(t):
|
||||||
|
value = TIME_FORMATS.get(t)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
for base in t.mro()[1:]:
|
||||||
|
value = TIME_FORMATS.get(base)
|
||||||
|
if value:
|
||||||
|
TIME_FORMATS[t] = value
|
||||||
|
return value
|
||||||
|
raise ValueError("Could not get time format for {0!r}".format(value))
|
||||||
|
|
||||||
|
|
||||||
|
class Cell(StyleableObject):
|
||||||
|
"""Describes cell associated properties.
|
||||||
|
|
||||||
|
Properties of interest include style, type, value, and address.
|
||||||
|
|
||||||
|
"""
|
||||||
|
__slots__ = (
|
||||||
|
'row',
|
||||||
|
'column',
|
||||||
|
'_value',
|
||||||
|
'data_type',
|
||||||
|
'parent',
|
||||||
|
'_hyperlink',
|
||||||
|
'_comment',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, worksheet, row=None, column=None, value=None, style_array=None):
|
||||||
|
super().__init__(worksheet, style_array)
|
||||||
|
self.row = row
|
||||||
|
"""Row number of this cell (1-based)"""
|
||||||
|
self.column = column
|
||||||
|
"""Column number of this cell (1-based)"""
|
||||||
|
# _value is the stored value, while value is the displayed value
|
||||||
|
self._value = None
|
||||||
|
self._hyperlink = None
|
||||||
|
self.data_type = 'n'
|
||||||
|
if value is not None:
|
||||||
|
self.value = value
|
||||||
|
self._comment = None
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coordinate(self):
|
||||||
|
"""This cell's coordinate (ex. 'A5')"""
|
||||||
|
col = get_column_letter(self.column)
|
||||||
|
return f"{col}{self.row}"
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def col_idx(self):
|
||||||
|
"""The numerical index of the column"""
|
||||||
|
return self.column
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column_letter(self):
|
||||||
|
return get_column_letter(self.column)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def encoding(self):
|
||||||
|
return self.parent.encoding
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_date(self):
|
||||||
|
return self.parent.parent.epoch
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Cell {0!r}.{1}>".format(self.parent.title, self.coordinate)
|
||||||
|
|
||||||
|
def check_string(self, value):
|
||||||
|
"""Check string coding, length, and line break character"""
|
||||||
|
if value is None:
|
||||||
|
return
|
||||||
|
# convert to str string
|
||||||
|
if not isinstance(value, str):
|
||||||
|
value = str(value, self.encoding)
|
||||||
|
value = str(value)
|
||||||
|
# string must never be longer than 32,767 characters
|
||||||
|
# truncate if necessary
|
||||||
|
value = value[:32767]
|
||||||
|
if next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
|
||||||
|
raise IllegalCharacterError(f"{value} cannot be used in worksheets.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def check_error(self, value):
|
||||||
|
"""Tries to convert Error" else N/A"""
|
||||||
|
try:
|
||||||
|
return str(value)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
return u'#N/A'
|
||||||
|
|
||||||
|
|
||||||
|
def _bind_value(self, value):
|
||||||
|
"""Given a value, infer the correct data type"""
|
||||||
|
|
||||||
|
self.data_type = "n"
|
||||||
|
t = type(value)
|
||||||
|
try:
|
||||||
|
dt = _TYPES[t]
|
||||||
|
except KeyError:
|
||||||
|
dt = get_type(t, value)
|
||||||
|
|
||||||
|
if dt is None and value is not None:
|
||||||
|
raise ValueError("Cannot convert {0!r} to Excel".format(value))
|
||||||
|
|
||||||
|
if dt:
|
||||||
|
self.data_type = dt
|
||||||
|
|
||||||
|
if dt == 'd':
|
||||||
|
if not is_date_format(self.number_format):
|
||||||
|
self.number_format = get_time_format(t)
|
||||||
|
|
||||||
|
elif dt == "s" and not isinstance(value, CellRichText):
|
||||||
|
value = self.check_string(value)
|
||||||
|
if len(value) > 1 and value.startswith("="):
|
||||||
|
self.data_type = 'f'
|
||||||
|
elif value in ERROR_CODES:
|
||||||
|
self.data_type = 'e'
|
||||||
|
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
"""Get or set the value held in the cell.
|
||||||
|
|
||||||
|
:type: depends on the value (string, float, int or
|
||||||
|
:class:`datetime.datetime`)
|
||||||
|
"""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
"""Set the value and infer type and display options."""
|
||||||
|
self._bind_value(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def internal_value(self):
|
||||||
|
"""Always returns the value for excel."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hyperlink(self):
|
||||||
|
"""Return the hyperlink target or an empty string"""
|
||||||
|
return self._hyperlink
|
||||||
|
|
||||||
|
|
||||||
|
@hyperlink.setter
|
||||||
|
def hyperlink(self, val):
|
||||||
|
"""Set value and display for hyperlinks in a cell.
|
||||||
|
Automatically sets the `value` of the cell with link text,
|
||||||
|
but you can modify it afterwards by setting the `value`
|
||||||
|
property, and the hyperlink will remain.
|
||||||
|
Hyperlink is removed if set to ``None``."""
|
||||||
|
if val is None:
|
||||||
|
self._hyperlink = None
|
||||||
|
else:
|
||||||
|
if not isinstance(val, Hyperlink):
|
||||||
|
val = Hyperlink(ref="", target=val)
|
||||||
|
val.ref = self.coordinate
|
||||||
|
self._hyperlink = val
|
||||||
|
if self._value is None:
|
||||||
|
self.value = val.target or val.location
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_date(self):
|
||||||
|
"""True if the value is formatted as a date
|
||||||
|
|
||||||
|
:type: bool
|
||||||
|
"""
|
||||||
|
return self.data_type == 'd' or (
|
||||||
|
self.data_type == 'n' and is_date_format(self.number_format)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def offset(self, row=0, column=0):
|
||||||
|
"""Returns a cell location relative to this cell.
|
||||||
|
|
||||||
|
:param row: number of rows to offset
|
||||||
|
:type row: int
|
||||||
|
|
||||||
|
:param column: number of columns to offset
|
||||||
|
:type column: int
|
||||||
|
|
||||||
|
:rtype: :class:`openpyxl.cell.Cell`
|
||||||
|
"""
|
||||||
|
offset_column = self.col_idx + column
|
||||||
|
offset_row = self.row + row
|
||||||
|
return self.parent.cell(column=offset_column, row=offset_row)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comment(self):
|
||||||
|
""" Returns the comment associated with this cell
|
||||||
|
|
||||||
|
:type: :class:`openpyxl.comments.Comment`
|
||||||
|
"""
|
||||||
|
return self._comment
|
||||||
|
|
||||||
|
|
||||||
|
@comment.setter
|
||||||
|
def comment(self, value):
|
||||||
|
"""
|
||||||
|
Assign a comment to a cell
|
||||||
|
"""
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
if value.parent:
|
||||||
|
value = copy(value)
|
||||||
|
value.bind(self)
|
||||||
|
elif value is None and self._comment:
|
||||||
|
self._comment.unbind()
|
||||||
|
self._comment = value
|
||||||
|
|
||||||
|
|
||||||
|
class MergedCell(StyleableObject):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Describes the properties of a cell in a merged cell and helps to
|
||||||
|
display the borders of the merged cell.
|
||||||
|
|
||||||
|
The value of a MergedCell is always None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ('row', 'column')
|
||||||
|
|
||||||
|
_value = None
|
||||||
|
data_type = "n"
|
||||||
|
comment = None
|
||||||
|
hyperlink = None
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, worksheet, row=None, column=None):
|
||||||
|
super().__init__(worksheet)
|
||||||
|
self.row = row
|
||||||
|
self.column = column
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<MergedCell {0!r}.{1}>".format(self.parent.title, self.coordinate)
|
||||||
|
|
||||||
|
coordinate = Cell.coordinate
|
||||||
|
_comment = comment
|
||||||
|
value = _value
|
||||||
|
|
||||||
|
|
||||||
|
def WriteOnlyCell(ws=None, value=None):
|
||||||
|
return Cell(worksheet=ws, column=1, row=1, value=value)
|
||||||
136
venv/lib/python3.12/site-packages/openpyxl/cell/read_only.py
Normal file
136
venv/lib/python3.12/site-packages/openpyxl/cell/read_only.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.cell import Cell
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
from openpyxl.utils.datetime import from_excel
|
||||||
|
from openpyxl.styles import is_date_format
|
||||||
|
from openpyxl.styles.numbers import BUILTIN_FORMATS, BUILTIN_FORMATS_MAX_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
class ReadOnlyCell:
|
||||||
|
|
||||||
|
__slots__ = ('parent', 'row', 'column', '_value', 'data_type', '_style_id')
|
||||||
|
|
||||||
|
def __init__(self, sheet, row, column, value, data_type='n', style_id=0):
|
||||||
|
self.parent = sheet
|
||||||
|
self._value = None
|
||||||
|
self.row = row
|
||||||
|
self.column = column
|
||||||
|
self.data_type = data_type
|
||||||
|
self.value = value
|
||||||
|
self._style_id = style_id
|
||||||
|
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
for a in self.__slots__:
|
||||||
|
if getattr(self, a) != getattr(other, a):
|
||||||
|
return
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<ReadOnlyCell {0!r}.{1}>".format(self.parent.title, self.coordinate)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coordinate(self):
|
||||||
|
column = get_column_letter(self.column)
|
||||||
|
return "{1}{0}".format(self.row, column)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coordinate(self):
|
||||||
|
return Cell.coordinate.__get__(self)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def column_letter(self):
|
||||||
|
return Cell.column_letter.__get__(self)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style_array(self):
|
||||||
|
return self.parent.parent._cell_styles[self._style_id]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_style(self):
|
||||||
|
return self._style_id != 0
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def number_format(self):
|
||||||
|
_id = self.style_array.numFmtId
|
||||||
|
if _id < BUILTIN_FORMATS_MAX_SIZE:
|
||||||
|
return BUILTIN_FORMATS.get(_id, "General")
|
||||||
|
else:
|
||||||
|
return self.parent.parent._number_formats[
|
||||||
|
_id - BUILTIN_FORMATS_MAX_SIZE]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def font(self):
|
||||||
|
_id = self.style_array.fontId
|
||||||
|
return self.parent.parent._fonts[_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fill(self):
|
||||||
|
_id = self.style_array.fillId
|
||||||
|
return self.parent.parent._fills[_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def border(self):
|
||||||
|
_id = self.style_array.borderId
|
||||||
|
return self.parent.parent._borders[_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alignment(self):
|
||||||
|
_id = self.style_array.alignmentId
|
||||||
|
return self.parent.parent._alignments[_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def protection(self):
|
||||||
|
_id = self.style_array.protectionId
|
||||||
|
return self.parent.parent._protections[_id]
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_date(self):
|
||||||
|
return Cell.is_date.__get__(self)
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def internal_value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
if self._value is not None:
|
||||||
|
raise AttributeError("Cell is read only")
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyCell:
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
value = None
|
||||||
|
is_date = False
|
||||||
|
font = None
|
||||||
|
border = None
|
||||||
|
fill = None
|
||||||
|
number_format = None
|
||||||
|
alignment = None
|
||||||
|
data_type = 'n'
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<EmptyCell>"
|
||||||
|
|
||||||
|
EMPTY_CELL = EmptyCell()
|
||||||
202
venv/lib/python3.12/site-packages/openpyxl/cell/rich_text.py
Normal file
202
venv/lib/python3.12/site-packages/openpyxl/cell/rich_text.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
"""
|
||||||
|
RichText definition
|
||||||
|
"""
|
||||||
|
from copy import copy
|
||||||
|
from openpyxl.compat import NUMERIC_TYPES
|
||||||
|
from openpyxl.cell.text import InlineFont, Text
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Strict,
|
||||||
|
String,
|
||||||
|
Typed
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.xml.functions import Element, whitespace
|
||||||
|
|
||||||
|
class TextBlock(Strict):
|
||||||
|
""" Represents text string in a specific format
|
||||||
|
|
||||||
|
This class is used as part of constructing a rich text strings.
|
||||||
|
"""
|
||||||
|
font = Typed(expected_type=InlineFont)
|
||||||
|
text = String()
|
||||||
|
|
||||||
|
def __init__(self, font, text):
|
||||||
|
self.font = font
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.text == other.text and self.font == other.font
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Just retun the text"""
|
||||||
|
return self.text
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
font = self.font != InlineFont() and self.font or "default"
|
||||||
|
return f"{self.__class__.__name__} text={self.text}, font={font}"
|
||||||
|
|
||||||
|
|
||||||
|
def to_tree(self):
|
||||||
|
el = Element("r")
|
||||||
|
el.append(self.font.to_tree(tagname="rPr"))
|
||||||
|
t = Element("t")
|
||||||
|
t.text = self.text
|
||||||
|
whitespace(t)
|
||||||
|
el.append(t)
|
||||||
|
return el
|
||||||
|
|
||||||
|
#
|
||||||
|
# Rich Text class.
|
||||||
|
# This class behaves just like a list whose members are either simple strings, or TextBlock() instances.
|
||||||
|
# In addition, it can be initialized in several ways:
|
||||||
|
# t = CellRFichText([...]) # initialize with a list.
|
||||||
|
# t = CellRFichText((...)) # initialize with a tuple.
|
||||||
|
# t = CellRichText(node) # where node is an Element() from either lxml or xml.etree (has a 'tag' element)
|
||||||
|
class CellRichText(list):
|
||||||
|
"""Represents a rich text string.
|
||||||
|
|
||||||
|
Initialize with a list made of pure strings or :class:`TextBlock` elements
|
||||||
|
Can index object to access or modify individual rich text elements
|
||||||
|
it also supports the + and += operators between rich text strings
|
||||||
|
There are no user methods for this class
|
||||||
|
|
||||||
|
operations which modify the string will generally call an optimization pass afterwards,
|
||||||
|
that merges text blocks with identical formats, consecutive pure text strings,
|
||||||
|
and remove empty strings and empty text blocks
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
if len(args) == 1:
|
||||||
|
args = args[0]
|
||||||
|
if isinstance(args, (list, tuple)):
|
||||||
|
CellRichText._check_rich_text(args)
|
||||||
|
else:
|
||||||
|
CellRichText._check_element(args)
|
||||||
|
args = [args]
|
||||||
|
else:
|
||||||
|
CellRichText._check_rich_text(args)
|
||||||
|
super().__init__(args)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_element(cls, value):
|
||||||
|
if not isinstance(value, (str, TextBlock, NUMERIC_TYPES)):
|
||||||
|
raise TypeError(f"Illegal CellRichText element {value}")
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_rich_text(cls, rich_text):
|
||||||
|
for t in rich_text:
|
||||||
|
CellRichText._check_element(t)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_tree(cls, node):
|
||||||
|
text = Text.from_tree(node)
|
||||||
|
if text.t:
|
||||||
|
return (text.t.replace('x005F_', ''),)
|
||||||
|
s = []
|
||||||
|
for r in text.r:
|
||||||
|
t = ""
|
||||||
|
if r.t:
|
||||||
|
t = r.t.replace('x005F_', '')
|
||||||
|
if r.rPr:
|
||||||
|
s.append(TextBlock(r.rPr, t))
|
||||||
|
else:
|
||||||
|
s.append(t)
|
||||||
|
return cls(s)
|
||||||
|
|
||||||
|
# Merge TextBlocks with identical formatting
|
||||||
|
# remove empty elements
|
||||||
|
def _opt(self):
|
||||||
|
last_t = None
|
||||||
|
l = CellRichText(tuple())
|
||||||
|
for t in self:
|
||||||
|
if isinstance(t, str):
|
||||||
|
if not t:
|
||||||
|
continue
|
||||||
|
elif not t.text:
|
||||||
|
continue
|
||||||
|
if type(last_t) == type(t):
|
||||||
|
if isinstance(t, str):
|
||||||
|
last_t += t
|
||||||
|
continue
|
||||||
|
elif last_t.font == t.font:
|
||||||
|
last_t.text += t.text
|
||||||
|
continue
|
||||||
|
if last_t:
|
||||||
|
l.append(last_t)
|
||||||
|
last_t = t
|
||||||
|
if last_t:
|
||||||
|
# Add remaining TextBlock at end of rich text
|
||||||
|
l.append(last_t)
|
||||||
|
super().__setitem__(slice(None), l)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def __iadd__(self, arg):
|
||||||
|
# copy used here to create new TextBlock() so we don't modify the right hand side in _opt()
|
||||||
|
CellRichText._check_rich_text(arg)
|
||||||
|
super().__iadd__([copy(e) for e in list(arg)])
|
||||||
|
return self._opt()
|
||||||
|
|
||||||
|
|
||||||
|
def __add__(self, arg):
|
||||||
|
return CellRichText([copy(e) for e in list(self) + list(arg)])._opt()
|
||||||
|
|
||||||
|
|
||||||
|
def __setitem__(self, indx, val):
|
||||||
|
CellRichText._check_element(val)
|
||||||
|
super().__setitem__(indx, val)
|
||||||
|
self._opt()
|
||||||
|
|
||||||
|
|
||||||
|
def append(self, arg):
|
||||||
|
CellRichText._check_element(arg)
|
||||||
|
super().append(arg)
|
||||||
|
|
||||||
|
|
||||||
|
def extend(self, arg):
|
||||||
|
CellRichText._check_rich_text(arg)
|
||||||
|
super().extend(arg)
|
||||||
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "CellRichText([{}])".format(', '.join((repr(s) for s in self)))
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ''.join([str(s) for s in self])
|
||||||
|
|
||||||
|
|
||||||
|
def as_list(self):
|
||||||
|
"""
|
||||||
|
Returns a list of the strings contained.
|
||||||
|
The main reason for this is to make editing easier.
|
||||||
|
"""
|
||||||
|
return [str(s) for s in self]
|
||||||
|
|
||||||
|
|
||||||
|
def to_tree(self):
|
||||||
|
"""
|
||||||
|
Return the full XML representation
|
||||||
|
"""
|
||||||
|
container = Element("is")
|
||||||
|
for obj in self:
|
||||||
|
if isinstance(obj, TextBlock):
|
||||||
|
container.append(obj.to_tree())
|
||||||
|
|
||||||
|
else:
|
||||||
|
el = Element("r")
|
||||||
|
t = Element("t")
|
||||||
|
t.text = obj
|
||||||
|
whitespace(t)
|
||||||
|
el.append(t)
|
||||||
|
container.append(el)
|
||||||
|
|
||||||
|
return container
|
||||||
|
|
||||||
184
venv/lib/python3.12/site-packages/openpyxl/cell/text.py
Normal file
184
venv/lib/python3.12/site-packages/openpyxl/cell/text.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
"""
|
||||||
|
Richtext definition
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Alias,
|
||||||
|
Typed,
|
||||||
|
Integer,
|
||||||
|
Set,
|
||||||
|
NoneSet,
|
||||||
|
Bool,
|
||||||
|
String,
|
||||||
|
Sequence,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedInteger,
|
||||||
|
NestedString,
|
||||||
|
NestedText,
|
||||||
|
)
|
||||||
|
from openpyxl.styles.fonts import Font
|
||||||
|
|
||||||
|
|
||||||
|
class PhoneticProperties(Serialisable):
|
||||||
|
|
||||||
|
tagname = "phoneticPr"
|
||||||
|
|
||||||
|
fontId = Integer()
|
||||||
|
type = NoneSet(values=(['halfwidthKatakana', 'fullwidthKatakana',
|
||||||
|
'Hiragana', 'noConversion']))
|
||||||
|
alignment = NoneSet(values=(['noControl', 'left', 'center', 'distributed']))
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
fontId=None,
|
||||||
|
type=None,
|
||||||
|
alignment=None,
|
||||||
|
):
|
||||||
|
self.fontId = fontId
|
||||||
|
self.type = type
|
||||||
|
self.alignment = alignment
|
||||||
|
|
||||||
|
|
||||||
|
class PhoneticText(Serialisable):
|
||||||
|
|
||||||
|
tagname = "rPh"
|
||||||
|
|
||||||
|
sb = Integer()
|
||||||
|
eb = Integer()
|
||||||
|
t = NestedText(expected_type=str)
|
||||||
|
text = Alias('t')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
sb=None,
|
||||||
|
eb=None,
|
||||||
|
t=None,
|
||||||
|
):
|
||||||
|
self.sb = sb
|
||||||
|
self.eb = eb
|
||||||
|
self.t = t
|
||||||
|
|
||||||
|
|
||||||
|
class InlineFont(Font):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Font for inline text because, yes what you need are different objects with the same elements but different constraints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tagname = "RPrElt"
|
||||||
|
|
||||||
|
rFont = NestedString(allow_none=True)
|
||||||
|
charset = Font.charset
|
||||||
|
family = Font.family
|
||||||
|
b =Font.b
|
||||||
|
i = Font.i
|
||||||
|
strike = Font.strike
|
||||||
|
outline = Font.outline
|
||||||
|
shadow = Font.shadow
|
||||||
|
condense = Font.condense
|
||||||
|
extend = Font.extend
|
||||||
|
color = Font.color
|
||||||
|
sz = Font.sz
|
||||||
|
u = Font.u
|
||||||
|
vertAlign = Font.vertAlign
|
||||||
|
scheme = Font.scheme
|
||||||
|
|
||||||
|
__elements__ = ('rFont', 'charset', 'family', 'b', 'i', 'strike',
|
||||||
|
'outline', 'shadow', 'condense', 'extend', 'color', 'sz', 'u',
|
||||||
|
'vertAlign', 'scheme')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
rFont=None,
|
||||||
|
charset=None,
|
||||||
|
family=None,
|
||||||
|
b=None,
|
||||||
|
i=None,
|
||||||
|
strike=None,
|
||||||
|
outline=None,
|
||||||
|
shadow=None,
|
||||||
|
condense=None,
|
||||||
|
extend=None,
|
||||||
|
color=None,
|
||||||
|
sz=None,
|
||||||
|
u=None,
|
||||||
|
vertAlign=None,
|
||||||
|
scheme=None,
|
||||||
|
):
|
||||||
|
self.rFont = rFont
|
||||||
|
self.charset = charset
|
||||||
|
self.family = family
|
||||||
|
self.b = b
|
||||||
|
self.i = i
|
||||||
|
self.strike = strike
|
||||||
|
self.outline = outline
|
||||||
|
self.shadow = shadow
|
||||||
|
self.condense = condense
|
||||||
|
self.extend = extend
|
||||||
|
self.color = color
|
||||||
|
self.sz = sz
|
||||||
|
self.u = u
|
||||||
|
self.vertAlign = vertAlign
|
||||||
|
self.scheme = scheme
|
||||||
|
|
||||||
|
|
||||||
|
class RichText(Serialisable):
|
||||||
|
|
||||||
|
tagname = "RElt"
|
||||||
|
|
||||||
|
rPr = Typed(expected_type=InlineFont, allow_none=True)
|
||||||
|
font = Alias("rPr")
|
||||||
|
t = NestedText(expected_type=str, allow_none=True)
|
||||||
|
text = Alias("t")
|
||||||
|
|
||||||
|
__elements__ = ('rPr', 't')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
rPr=None,
|
||||||
|
t=None,
|
||||||
|
):
|
||||||
|
self.rPr = rPr
|
||||||
|
self.t = t
|
||||||
|
|
||||||
|
|
||||||
|
class Text(Serialisable):
|
||||||
|
|
||||||
|
tagname = "text"
|
||||||
|
|
||||||
|
t = NestedText(allow_none=True, expected_type=str)
|
||||||
|
plain = Alias("t")
|
||||||
|
r = Sequence(expected_type=RichText, allow_none=True)
|
||||||
|
formatted = Alias("r")
|
||||||
|
rPh = Sequence(expected_type=PhoneticText, allow_none=True)
|
||||||
|
phonetic = Alias("rPh")
|
||||||
|
phoneticPr = Typed(expected_type=PhoneticProperties, allow_none=True)
|
||||||
|
PhoneticProperties = Alias("phoneticPr")
|
||||||
|
|
||||||
|
__elements__ = ('t', 'r', 'rPh', 'phoneticPr')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
t=None,
|
||||||
|
r=(),
|
||||||
|
rPh=(),
|
||||||
|
phoneticPr=None,
|
||||||
|
):
|
||||||
|
self.t = t
|
||||||
|
self.r = r
|
||||||
|
self.rPh = rPh
|
||||||
|
self.phoneticPr = phoneticPr
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self):
|
||||||
|
"""
|
||||||
|
Text stripped of all formatting
|
||||||
|
"""
|
||||||
|
snippets = []
|
||||||
|
if self.plain is not None:
|
||||||
|
snippets.append(self.plain)
|
||||||
|
for block in self.formatted:
|
||||||
|
if block.t is not None:
|
||||||
|
snippets.append(block.t)
|
||||||
|
return u"".join(snippets)
|
||||||
105
venv/lib/python3.12/site-packages/openpyxl/chart/_3d.py
Normal file
105
venv/lib/python3.12/site-packages/openpyxl/chart/_3d.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors import Typed, Alias
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedInteger,
|
||||||
|
NestedMinMax,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from .marker import PictureOptions
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
|
||||||
|
|
||||||
|
class View3D(Serialisable):
|
||||||
|
|
||||||
|
tagname = "view3D"
|
||||||
|
|
||||||
|
rotX = NestedMinMax(min=-90, max=90, allow_none=True)
|
||||||
|
x_rotation = Alias('rotX')
|
||||||
|
hPercent = NestedMinMax(min=5, max=500, allow_none=True)
|
||||||
|
height_percent = Alias('hPercent')
|
||||||
|
rotY = NestedInteger(min=-90, max=90, allow_none=True)
|
||||||
|
y_rotation = Alias('rotY')
|
||||||
|
depthPercent = NestedInteger(allow_none=True)
|
||||||
|
rAngAx = NestedBool(allow_none=True)
|
||||||
|
right_angle_axes = Alias('rAngAx')
|
||||||
|
perspective = NestedInteger(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('rotX', 'hPercent', 'rotY', 'depthPercent', 'rAngAx',
|
||||||
|
'perspective',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
rotX=15,
|
||||||
|
hPercent=None,
|
||||||
|
rotY=20,
|
||||||
|
depthPercent=None,
|
||||||
|
rAngAx=True,
|
||||||
|
perspective=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.rotX = rotX
|
||||||
|
self.hPercent = hPercent
|
||||||
|
self.rotY = rotY
|
||||||
|
self.depthPercent = depthPercent
|
||||||
|
self.rAngAx = rAngAx
|
||||||
|
self.perspective = perspective
|
||||||
|
|
||||||
|
|
||||||
|
class Surface(Serialisable):
|
||||||
|
|
||||||
|
tagname = "surface"
|
||||||
|
|
||||||
|
thickness = NestedInteger(allow_none=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
pictureOptions = Typed(expected_type=PictureOptions, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('thickness', 'spPr', 'pictureOptions',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
thickness=None,
|
||||||
|
spPr=None,
|
||||||
|
pictureOptions=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.thickness = thickness
|
||||||
|
self.spPr = spPr
|
||||||
|
self.pictureOptions = pictureOptions
|
||||||
|
|
||||||
|
|
||||||
|
class _3DBase(Serialisable):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base class for 3D charts
|
||||||
|
"""
|
||||||
|
|
||||||
|
tagname = "ChartBase"
|
||||||
|
|
||||||
|
view3D = Typed(expected_type=View3D, allow_none=True)
|
||||||
|
floor = Typed(expected_type=Surface, allow_none=True)
|
||||||
|
sideWall = Typed(expected_type=Surface, allow_none=True)
|
||||||
|
backWall = Typed(expected_type=Surface, allow_none=True)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
view3D=None,
|
||||||
|
floor=None,
|
||||||
|
sideWall=None,
|
||||||
|
backWall=None,
|
||||||
|
):
|
||||||
|
if view3D is None:
|
||||||
|
view3D = View3D()
|
||||||
|
self.view3D = view3D
|
||||||
|
if floor is None:
|
||||||
|
floor = Surface()
|
||||||
|
self.floor = floor
|
||||||
|
if sideWall is None:
|
||||||
|
sideWall = Surface()
|
||||||
|
self.sideWall = sideWall
|
||||||
|
if backWall is None:
|
||||||
|
backWall = Surface()
|
||||||
|
self.backWall = backWall
|
||||||
|
super(_3DBase, self).__init__()
|
||||||
19
venv/lib/python3.12/site-packages/openpyxl/chart/__init__.py
Normal file
19
venv/lib/python3.12/site-packages/openpyxl/chart/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from .area_chart import AreaChart, AreaChart3D
|
||||||
|
from .bar_chart import BarChart, BarChart3D
|
||||||
|
from .bubble_chart import BubbleChart
|
||||||
|
from .line_chart import LineChart, LineChart3D
|
||||||
|
from .pie_chart import (
|
||||||
|
PieChart,
|
||||||
|
PieChart3D,
|
||||||
|
DoughnutChart,
|
||||||
|
ProjectedPieChart
|
||||||
|
)
|
||||||
|
from .radar_chart import RadarChart
|
||||||
|
from .scatter_chart import ScatterChart
|
||||||
|
from .stock_chart import StockChart
|
||||||
|
from .surface_chart import SurfaceChart, SurfaceChart3D
|
||||||
|
|
||||||
|
from .series_factory import SeriesFactory as Series
|
||||||
|
from .reference import Reference
|
||||||
199
venv/lib/python3.12/site-packages/openpyxl/chart/_chart.py
Normal file
199
venv/lib/python3.12/site-packages/openpyxl/chart/_chart.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Integer,
|
||||||
|
Alias,
|
||||||
|
MinMax,
|
||||||
|
Bool,
|
||||||
|
Set,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.sequence import ValueSequence
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
|
||||||
|
from ._3d import _3DBase
|
||||||
|
from .data_source import AxDataSource, NumRef
|
||||||
|
from .layout import Layout
|
||||||
|
from .legend import Legend
|
||||||
|
from .reference import Reference
|
||||||
|
from .series_factory import SeriesFactory
|
||||||
|
from .series import attribute_mapping
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .title import TitleDescriptor
|
||||||
|
|
||||||
|
class AxId(Serialisable):
|
||||||
|
|
||||||
|
val = Integer()
|
||||||
|
|
||||||
|
def __init__(self, val):
|
||||||
|
self.val = val
|
||||||
|
|
||||||
|
|
||||||
|
def PlotArea():
|
||||||
|
from .chartspace import PlotArea
|
||||||
|
return PlotArea()
|
||||||
|
|
||||||
|
|
||||||
|
class ChartBase(Serialisable):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base class for all charts
|
||||||
|
"""
|
||||||
|
|
||||||
|
legend = Typed(expected_type=Legend, allow_none=True)
|
||||||
|
layout = Typed(expected_type=Layout, allow_none=True)
|
||||||
|
roundedCorners = Bool(allow_none=True)
|
||||||
|
axId = ValueSequence(expected_type=int)
|
||||||
|
visible_cells_only = Bool(allow_none=True)
|
||||||
|
display_blanks = Set(values=['span', 'gap', 'zero'])
|
||||||
|
graphical_properties = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
|
||||||
|
_series_type = ""
|
||||||
|
ser = ()
|
||||||
|
series = Alias('ser')
|
||||||
|
title = TitleDescriptor()
|
||||||
|
anchor = "E15" # default anchor position
|
||||||
|
width = 15 # in cm, approx 5 rows
|
||||||
|
height = 7.5 # in cm, approx 14 rows
|
||||||
|
_id = 1
|
||||||
|
_path = "/xl/charts/chart{0}.xml"
|
||||||
|
style = MinMax(allow_none=True, min=1, max=48)
|
||||||
|
mime_type = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
|
||||||
|
graphical_properties = Typed(expected_type=GraphicalProperties, allow_none=True) # mapped to chartspace
|
||||||
|
|
||||||
|
__elements__ = ()
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, axId=(), **kw):
|
||||||
|
self._charts = [self]
|
||||||
|
self.title = None
|
||||||
|
self.layout = None
|
||||||
|
self.roundedCorners = None
|
||||||
|
self.legend = Legend()
|
||||||
|
self.graphical_properties = None
|
||||||
|
self.style = None
|
||||||
|
self.plot_area = PlotArea()
|
||||||
|
self.axId = axId
|
||||||
|
self.display_blanks = 'gap'
|
||||||
|
self.pivotSource = None
|
||||||
|
self.pivotFormats = ()
|
||||||
|
self.visible_cells_only = True
|
||||||
|
self.idx_base = 0
|
||||||
|
self.graphical_properties = None
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""
|
||||||
|
Just need to check for identity
|
||||||
|
"""
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
def __iadd__(self, other):
|
||||||
|
"""
|
||||||
|
Combine the chart with another one
|
||||||
|
"""
|
||||||
|
if not isinstance(other, ChartBase):
|
||||||
|
raise TypeError("Only other charts can be added")
|
||||||
|
self._charts.append(other)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def to_tree(self, namespace=None, tagname=None, idx=None):
|
||||||
|
self.axId = [id for id in self._axes]
|
||||||
|
if self.ser is not None:
|
||||||
|
for s in self.ser:
|
||||||
|
s.__elements__ = attribute_mapping[self._series_type]
|
||||||
|
return super().to_tree(tagname, idx)
|
||||||
|
|
||||||
|
|
||||||
|
def _reindex(self):
|
||||||
|
"""
|
||||||
|
Normalise and rebase series: sort by order and then rebase order
|
||||||
|
|
||||||
|
"""
|
||||||
|
# sort data series in order and rebase
|
||||||
|
ds = sorted(self.series, key=attrgetter("order"))
|
||||||
|
for idx, s in enumerate(ds):
|
||||||
|
s.order = idx
|
||||||
|
self.series = ds
|
||||||
|
|
||||||
|
|
||||||
|
def _write(self):
|
||||||
|
from .chartspace import ChartSpace, ChartContainer
|
||||||
|
self.plot_area.layout = self.layout
|
||||||
|
|
||||||
|
idx_base = self.idx_base
|
||||||
|
for chart in self._charts:
|
||||||
|
if chart not in self.plot_area._charts:
|
||||||
|
chart.idx_base = idx_base
|
||||||
|
idx_base += len(chart.series)
|
||||||
|
self.plot_area._charts = self._charts
|
||||||
|
|
||||||
|
container = ChartContainer(plotArea=self.plot_area, legend=self.legend, title=self.title)
|
||||||
|
if isinstance(chart, _3DBase):
|
||||||
|
container.view3D = chart.view3D
|
||||||
|
container.floor = chart.floor
|
||||||
|
container.sideWall = chart.sideWall
|
||||||
|
container.backWall = chart.backWall
|
||||||
|
container.plotVisOnly = self.visible_cells_only
|
||||||
|
container.dispBlanksAs = self.display_blanks
|
||||||
|
container.pivotFmts = self.pivotFormats
|
||||||
|
cs = ChartSpace(chart=container)
|
||||||
|
cs.style = self.style
|
||||||
|
cs.roundedCorners = self.roundedCorners
|
||||||
|
cs.pivotSource = self.pivotSource
|
||||||
|
cs.spPr = self.graphical_properties
|
||||||
|
return cs.to_tree()
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _axes(self):
|
||||||
|
x = getattr(self, "x_axis", None)
|
||||||
|
y = getattr(self, "y_axis", None)
|
||||||
|
z = getattr(self, "z_axis", None)
|
||||||
|
return OrderedDict([(axis.axId, axis) for axis in (x, y, z) if axis])
|
||||||
|
|
||||||
|
|
||||||
|
def set_categories(self, labels):
|
||||||
|
"""
|
||||||
|
Set the categories / x-axis values
|
||||||
|
"""
|
||||||
|
if not isinstance(labels, Reference):
|
||||||
|
labels = Reference(range_string=labels)
|
||||||
|
for s in self.ser:
|
||||||
|
s.cat = AxDataSource(numRef=NumRef(f=labels))
|
||||||
|
|
||||||
|
|
||||||
|
def add_data(self, data, from_rows=False, titles_from_data=False):
|
||||||
|
"""
|
||||||
|
Add a range of data in a single pass.
|
||||||
|
The default is to treat each column as a data series.
|
||||||
|
"""
|
||||||
|
if not isinstance(data, Reference):
|
||||||
|
data = Reference(range_string=data)
|
||||||
|
|
||||||
|
if from_rows:
|
||||||
|
values = data.rows
|
||||||
|
|
||||||
|
else:
|
||||||
|
values = data.cols
|
||||||
|
|
||||||
|
for ref in values:
|
||||||
|
series = SeriesFactory(ref, title_from_data=titles_from_data)
|
||||||
|
self.series.append(series)
|
||||||
|
|
||||||
|
|
||||||
|
def append(self, value):
|
||||||
|
"""Append a data series to the chart"""
|
||||||
|
l = self.series[:]
|
||||||
|
l.append(value)
|
||||||
|
self.series = l
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self._path.format(self._id)
|
||||||
106
venv/lib/python3.12/site-packages/openpyxl/chart/area_chart.py
Normal file
106
venv/lib/python3.12/site-packages/openpyxl/chart/area_chart.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Set,
|
||||||
|
Bool,
|
||||||
|
Integer,
|
||||||
|
Sequence,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedMinMax,
|
||||||
|
NestedSet,
|
||||||
|
NestedBool,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ._chart import ChartBase
|
||||||
|
from .descriptors import NestedGapAmount
|
||||||
|
from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines
|
||||||
|
from .label import DataLabelList
|
||||||
|
from .series import Series
|
||||||
|
|
||||||
|
|
||||||
|
class _AreaChartBase(ChartBase):
|
||||||
|
|
||||||
|
grouping = NestedSet(values=(['percentStacked', 'standard', 'stacked']))
|
||||||
|
varyColors = NestedBool(nested=True, allow_none=True)
|
||||||
|
ser = Sequence(expected_type=Series, allow_none=True)
|
||||||
|
dLbls = Typed(expected_type=DataLabelList, allow_none=True)
|
||||||
|
dataLabels = Alias("dLbls")
|
||||||
|
dropLines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
|
||||||
|
_series_type = "area"
|
||||||
|
|
||||||
|
__elements__ = ('grouping', 'varyColors', 'ser', 'dLbls', 'dropLines')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
grouping="standard",
|
||||||
|
varyColors=None,
|
||||||
|
ser=(),
|
||||||
|
dLbls=None,
|
||||||
|
dropLines=None,
|
||||||
|
):
|
||||||
|
self.grouping = grouping
|
||||||
|
self.varyColors = varyColors
|
||||||
|
self.ser = ser
|
||||||
|
self.dLbls = dLbls
|
||||||
|
self.dropLines = dropLines
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
class AreaChart(_AreaChartBase):
|
||||||
|
|
||||||
|
tagname = "areaChart"
|
||||||
|
|
||||||
|
grouping = _AreaChartBase.grouping
|
||||||
|
varyColors = _AreaChartBase.varyColors
|
||||||
|
ser = _AreaChartBase.ser
|
||||||
|
dLbls = _AreaChartBase.dLbls
|
||||||
|
dropLines = _AreaChartBase.dropLines
|
||||||
|
|
||||||
|
# chart properties actually used by containing classes
|
||||||
|
x_axis = Typed(expected_type=TextAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _AreaChartBase.__elements__ + ('axId',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
axId=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.x_axis = TextAxis()
|
||||||
|
self.y_axis = NumericAxis()
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class AreaChart3D(AreaChart):
|
||||||
|
|
||||||
|
tagname = "area3DChart"
|
||||||
|
|
||||||
|
grouping = _AreaChartBase.grouping
|
||||||
|
varyColors = _AreaChartBase.varyColors
|
||||||
|
ser = _AreaChartBase.ser
|
||||||
|
dLbls = _AreaChartBase.dLbls
|
||||||
|
dropLines = _AreaChartBase.dropLines
|
||||||
|
|
||||||
|
gapDepth = NestedGapAmount()
|
||||||
|
|
||||||
|
x_axis = Typed(expected_type=TextAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
z_axis = Typed(expected_type=SeriesAxis, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = AreaChart.__elements__ + ('gapDepth', )
|
||||||
|
|
||||||
|
def __init__(self, gapDepth=None, **kw):
|
||||||
|
self.gapDepth = gapDepth
|
||||||
|
super(AreaChart3D, self).__init__(**kw)
|
||||||
|
self.x_axis = TextAxis()
|
||||||
|
self.y_axis = NumericAxis()
|
||||||
|
self.z_axis = SeriesAxis()
|
||||||
401
venv/lib/python3.12/site-packages/openpyxl/chart/axis.py
Normal file
401
venv/lib/python3.12/site-packages/openpyxl/chart/axis.py
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Float,
|
||||||
|
NoneSet,
|
||||||
|
Bool,
|
||||||
|
Integer,
|
||||||
|
MinMax,
|
||||||
|
NoneSet,
|
||||||
|
Set,
|
||||||
|
String,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.descriptors.excel import (
|
||||||
|
ExtensionList,
|
||||||
|
Percentage,
|
||||||
|
_explicit_none,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedValue,
|
||||||
|
NestedSet,
|
||||||
|
NestedBool,
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedFloat,
|
||||||
|
NestedInteger,
|
||||||
|
NestedMinMax,
|
||||||
|
)
|
||||||
|
from openpyxl.xml.constants import CHART_NS
|
||||||
|
|
||||||
|
from .descriptors import NumberFormatDescriptor
|
||||||
|
from .layout import Layout
|
||||||
|
from .text import Text, RichText
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .title import Title, TitleDescriptor
|
||||||
|
|
||||||
|
|
||||||
|
class ChartLines(Serialisable):
|
||||||
|
|
||||||
|
tagname = "chartLines"
|
||||||
|
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
|
||||||
|
def __init__(self, spPr=None):
|
||||||
|
self.spPr = spPr
|
||||||
|
|
||||||
|
|
||||||
|
class Scaling(Serialisable):
|
||||||
|
|
||||||
|
tagname = "scaling"
|
||||||
|
|
||||||
|
logBase = NestedFloat(allow_none=True)
|
||||||
|
orientation = NestedSet(values=(['maxMin', 'minMax']))
|
||||||
|
max = NestedFloat(allow_none=True)
|
||||||
|
min = NestedFloat(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('logBase', 'orientation', 'max', 'min',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
logBase=None,
|
||||||
|
orientation="minMax",
|
||||||
|
max=None,
|
||||||
|
min=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.logBase = logBase
|
||||||
|
self.orientation = orientation
|
||||||
|
self.max = max
|
||||||
|
self.min = min
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseAxis(Serialisable):
|
||||||
|
|
||||||
|
axId = NestedInteger(expected_type=int)
|
||||||
|
scaling = Typed(expected_type=Scaling)
|
||||||
|
delete = NestedBool(allow_none=True)
|
||||||
|
axPos = NestedSet(values=(['b', 'l', 'r', 't']))
|
||||||
|
majorGridlines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
minorGridlines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
title = TitleDescriptor()
|
||||||
|
numFmt = NumberFormatDescriptor()
|
||||||
|
number_format = Alias("numFmt")
|
||||||
|
majorTickMark = NestedNoneSet(values=(['cross', 'in', 'out']), to_tree=_explicit_none)
|
||||||
|
minorTickMark = NestedNoneSet(values=(['cross', 'in', 'out']), to_tree=_explicit_none)
|
||||||
|
tickLblPos = NestedNoneSet(values=(['high', 'low', 'nextTo']))
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
textProperties = Alias('txPr')
|
||||||
|
crossAx = NestedInteger(expected_type=int) # references other axis
|
||||||
|
crosses = NestedNoneSet(values=(['autoZero', 'max', 'min']))
|
||||||
|
crossesAt = NestedFloat(allow_none=True)
|
||||||
|
|
||||||
|
# crosses & crossesAt are mutually exclusive
|
||||||
|
|
||||||
|
__elements__ = ('axId', 'scaling', 'delete', 'axPos', 'majorGridlines',
|
||||||
|
'minorGridlines', 'title', 'numFmt', 'majorTickMark', 'minorTickMark',
|
||||||
|
'tickLblPos', 'spPr', 'txPr', 'crossAx', 'crosses', 'crossesAt')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
axId=None,
|
||||||
|
scaling=None,
|
||||||
|
delete=None,
|
||||||
|
axPos='l',
|
||||||
|
majorGridlines=None,
|
||||||
|
minorGridlines=None,
|
||||||
|
title=None,
|
||||||
|
numFmt=None,
|
||||||
|
majorTickMark=None,
|
||||||
|
minorTickMark=None,
|
||||||
|
tickLblPos=None,
|
||||||
|
spPr=None,
|
||||||
|
txPr= None,
|
||||||
|
crossAx=None,
|
||||||
|
crosses=None,
|
||||||
|
crossesAt=None,
|
||||||
|
):
|
||||||
|
self.axId = axId
|
||||||
|
if scaling is None:
|
||||||
|
scaling = Scaling()
|
||||||
|
self.scaling = scaling
|
||||||
|
self.delete = delete
|
||||||
|
self.axPos = axPos
|
||||||
|
self.majorGridlines = majorGridlines
|
||||||
|
self.minorGridlines = minorGridlines
|
||||||
|
self.title = title
|
||||||
|
self.numFmt = numFmt
|
||||||
|
self.majorTickMark = majorTickMark
|
||||||
|
self.minorTickMark = minorTickMark
|
||||||
|
self.tickLblPos = tickLblPos
|
||||||
|
self.spPr = spPr
|
||||||
|
self.txPr = txPr
|
||||||
|
self.crossAx = crossAx
|
||||||
|
self.crosses = crosses
|
||||||
|
self.crossesAt = crossesAt
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayUnitsLabel(Serialisable):
|
||||||
|
|
||||||
|
tagname = "dispUnitsLbl"
|
||||||
|
|
||||||
|
layout = Typed(expected_type=Layout, allow_none=True)
|
||||||
|
tx = Typed(expected_type=Text, allow_none=True)
|
||||||
|
text = Alias("tx")
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias("spPr")
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
textPropertes = Alias("txPr")
|
||||||
|
|
||||||
|
__elements__ = ('layout', 'tx', 'spPr', 'txPr')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
layout=None,
|
||||||
|
tx=None,
|
||||||
|
spPr=None,
|
||||||
|
txPr=None,
|
||||||
|
):
|
||||||
|
self.layout = layout
|
||||||
|
self.tx = tx
|
||||||
|
self.spPr = spPr
|
||||||
|
self.txPr = txPr
|
||||||
|
|
||||||
|
|
||||||
|
class DisplayUnitsLabelList(Serialisable):
|
||||||
|
|
||||||
|
tagname = "dispUnits"
|
||||||
|
|
||||||
|
custUnit = NestedFloat(allow_none=True)
|
||||||
|
builtInUnit = NestedNoneSet(values=(['hundreds', 'thousands',
|
||||||
|
'tenThousands', 'hundredThousands', 'millions', 'tenMillions',
|
||||||
|
'hundredMillions', 'billions', 'trillions']))
|
||||||
|
dispUnitsLbl = Typed(expected_type=DisplayUnitsLabel, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('custUnit', 'builtInUnit', 'dispUnitsLbl',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
custUnit=None,
|
||||||
|
builtInUnit=None,
|
||||||
|
dispUnitsLbl=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.custUnit = custUnit
|
||||||
|
self.builtInUnit = builtInUnit
|
||||||
|
self.dispUnitsLbl = dispUnitsLbl
|
||||||
|
|
||||||
|
|
||||||
|
class NumericAxis(_BaseAxis):
|
||||||
|
|
||||||
|
tagname = "valAx"
|
||||||
|
|
||||||
|
axId = _BaseAxis.axId
|
||||||
|
scaling = _BaseAxis.scaling
|
||||||
|
delete = _BaseAxis.delete
|
||||||
|
axPos = _BaseAxis.axPos
|
||||||
|
majorGridlines = _BaseAxis.majorGridlines
|
||||||
|
minorGridlines = _BaseAxis.minorGridlines
|
||||||
|
title = _BaseAxis.title
|
||||||
|
numFmt = _BaseAxis.numFmt
|
||||||
|
majorTickMark = _BaseAxis.majorTickMark
|
||||||
|
minorTickMark = _BaseAxis.minorTickMark
|
||||||
|
tickLblPos = _BaseAxis.tickLblPos
|
||||||
|
spPr = _BaseAxis.spPr
|
||||||
|
txPr = _BaseAxis.txPr
|
||||||
|
crossAx = _BaseAxis.crossAx
|
||||||
|
crosses = _BaseAxis.crosses
|
||||||
|
crossesAt = _BaseAxis.crossesAt
|
||||||
|
|
||||||
|
crossBetween = NestedNoneSet(values=(['between', 'midCat']))
|
||||||
|
majorUnit = NestedFloat(allow_none=True)
|
||||||
|
minorUnit = NestedFloat(allow_none=True)
|
||||||
|
dispUnits = Typed(expected_type=DisplayUnitsLabelList, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _BaseAxis.__elements__ + ('crossBetween', 'majorUnit',
|
||||||
|
'minorUnit', 'dispUnits',)
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
crossBetween=None,
|
||||||
|
majorUnit=None,
|
||||||
|
minorUnit=None,
|
||||||
|
dispUnits=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.crossBetween = crossBetween
|
||||||
|
self.majorUnit = majorUnit
|
||||||
|
self.minorUnit = minorUnit
|
||||||
|
self.dispUnits = dispUnits
|
||||||
|
kw.setdefault('majorGridlines', ChartLines())
|
||||||
|
kw.setdefault('axId', 100)
|
||||||
|
kw.setdefault('crossAx', 10)
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_tree(cls, node):
|
||||||
|
"""
|
||||||
|
Special case value axes with no gridlines
|
||||||
|
"""
|
||||||
|
self = super().from_tree(node)
|
||||||
|
gridlines = node.find("{%s}majorGridlines" % CHART_NS)
|
||||||
|
if gridlines is None:
|
||||||
|
self.majorGridlines = None
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TextAxis(_BaseAxis):
|
||||||
|
|
||||||
|
tagname = "catAx"
|
||||||
|
|
||||||
|
axId = _BaseAxis.axId
|
||||||
|
scaling = _BaseAxis.scaling
|
||||||
|
delete = _BaseAxis.delete
|
||||||
|
axPos = _BaseAxis.axPos
|
||||||
|
majorGridlines = _BaseAxis.majorGridlines
|
||||||
|
minorGridlines = _BaseAxis.minorGridlines
|
||||||
|
title = _BaseAxis.title
|
||||||
|
numFmt = _BaseAxis.numFmt
|
||||||
|
majorTickMark = _BaseAxis.majorTickMark
|
||||||
|
minorTickMark = _BaseAxis.minorTickMark
|
||||||
|
tickLblPos = _BaseAxis.tickLblPos
|
||||||
|
spPr = _BaseAxis.spPr
|
||||||
|
txPr = _BaseAxis.txPr
|
||||||
|
crossAx = _BaseAxis.crossAx
|
||||||
|
crosses = _BaseAxis.crosses
|
||||||
|
crossesAt = _BaseAxis.crossesAt
|
||||||
|
|
||||||
|
auto = NestedBool(allow_none=True)
|
||||||
|
lblAlgn = NestedNoneSet(values=(['ctr', 'l', 'r']))
|
||||||
|
lblOffset = NestedMinMax(min=0, max=1000)
|
||||||
|
tickLblSkip = NestedInteger(allow_none=True)
|
||||||
|
tickMarkSkip = NestedInteger(allow_none=True)
|
||||||
|
noMultiLvlLbl = NestedBool(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _BaseAxis.__elements__ + ('auto', 'lblAlgn', 'lblOffset',
|
||||||
|
'tickLblSkip', 'tickMarkSkip', 'noMultiLvlLbl')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
auto=None,
|
||||||
|
lblAlgn=None,
|
||||||
|
lblOffset=100,
|
||||||
|
tickLblSkip=None,
|
||||||
|
tickMarkSkip=None,
|
||||||
|
noMultiLvlLbl=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.auto = auto
|
||||||
|
self.lblAlgn = lblAlgn
|
||||||
|
self.lblOffset = lblOffset
|
||||||
|
self.tickLblSkip = tickLblSkip
|
||||||
|
self.tickMarkSkip = tickMarkSkip
|
||||||
|
self.noMultiLvlLbl = noMultiLvlLbl
|
||||||
|
kw.setdefault('axId', 10)
|
||||||
|
kw.setdefault('crossAx', 100)
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class DateAxis(TextAxis):
|
||||||
|
|
||||||
|
tagname = "dateAx"
|
||||||
|
|
||||||
|
axId = _BaseAxis.axId
|
||||||
|
scaling = _BaseAxis.scaling
|
||||||
|
delete = _BaseAxis.delete
|
||||||
|
axPos = _BaseAxis.axPos
|
||||||
|
majorGridlines = _BaseAxis.majorGridlines
|
||||||
|
minorGridlines = _BaseAxis.minorGridlines
|
||||||
|
title = _BaseAxis.title
|
||||||
|
numFmt = _BaseAxis.numFmt
|
||||||
|
majorTickMark = _BaseAxis.majorTickMark
|
||||||
|
minorTickMark = _BaseAxis.minorTickMark
|
||||||
|
tickLblPos = _BaseAxis.tickLblPos
|
||||||
|
spPr = _BaseAxis.spPr
|
||||||
|
txPr = _BaseAxis.txPr
|
||||||
|
crossAx = _BaseAxis.crossAx
|
||||||
|
crosses = _BaseAxis.crosses
|
||||||
|
crossesAt = _BaseAxis.crossesAt
|
||||||
|
|
||||||
|
auto = NestedBool(allow_none=True)
|
||||||
|
lblOffset = NestedInteger(allow_none=True)
|
||||||
|
baseTimeUnit = NestedNoneSet(values=(['days', 'months', 'years']))
|
||||||
|
majorUnit = NestedFloat(allow_none=True)
|
||||||
|
majorTimeUnit = NestedNoneSet(values=(['days', 'months', 'years']))
|
||||||
|
minorUnit = NestedFloat(allow_none=True)
|
||||||
|
minorTimeUnit = NestedNoneSet(values=(['days', 'months', 'years']))
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _BaseAxis.__elements__ + ('auto', 'lblOffset',
|
||||||
|
'baseTimeUnit', 'majorUnit', 'majorTimeUnit', 'minorUnit',
|
||||||
|
'minorTimeUnit')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
auto=None,
|
||||||
|
lblOffset=None,
|
||||||
|
baseTimeUnit=None,
|
||||||
|
majorUnit=None,
|
||||||
|
majorTimeUnit=None,
|
||||||
|
minorUnit=None,
|
||||||
|
minorTimeUnit=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.auto = auto
|
||||||
|
self.lblOffset = lblOffset
|
||||||
|
self.baseTimeUnit = baseTimeUnit
|
||||||
|
self.majorUnit = majorUnit
|
||||||
|
self.majorTimeUnit = majorTimeUnit
|
||||||
|
self.minorUnit = minorUnit
|
||||||
|
self.minorTimeUnit = minorTimeUnit
|
||||||
|
kw.setdefault('axId', 500)
|
||||||
|
kw.setdefault('lblOffset', lblOffset)
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class SeriesAxis(_BaseAxis):
|
||||||
|
|
||||||
|
tagname = "serAx"
|
||||||
|
|
||||||
|
axId = _BaseAxis.axId
|
||||||
|
scaling = _BaseAxis.scaling
|
||||||
|
delete = _BaseAxis.delete
|
||||||
|
axPos = _BaseAxis.axPos
|
||||||
|
majorGridlines = _BaseAxis.majorGridlines
|
||||||
|
minorGridlines = _BaseAxis.minorGridlines
|
||||||
|
title = _BaseAxis.title
|
||||||
|
numFmt = _BaseAxis.numFmt
|
||||||
|
majorTickMark = _BaseAxis.majorTickMark
|
||||||
|
minorTickMark = _BaseAxis.minorTickMark
|
||||||
|
tickLblPos = _BaseAxis.tickLblPos
|
||||||
|
spPr = _BaseAxis.spPr
|
||||||
|
txPr = _BaseAxis.txPr
|
||||||
|
crossAx = _BaseAxis.crossAx
|
||||||
|
crosses = _BaseAxis.crosses
|
||||||
|
crossesAt = _BaseAxis.crossesAt
|
||||||
|
|
||||||
|
tickLblSkip = NestedInteger(allow_none=True)
|
||||||
|
tickMarkSkip = NestedInteger(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _BaseAxis.__elements__ + ('tickLblSkip', 'tickMarkSkip')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
tickLblSkip=None,
|
||||||
|
tickMarkSkip=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.tickLblSkip = tickLblSkip
|
||||||
|
self.tickMarkSkip = tickMarkSkip
|
||||||
|
kw.setdefault('axId', 1000)
|
||||||
|
kw.setdefault('crossAx', 10)
|
||||||
|
super().__init__(**kw)
|
||||||
144
venv/lib/python3.12/site-packages/openpyxl/chart/bar_chart.py
Normal file
144
venv/lib/python3.12/site-packages/openpyxl/chart/bar_chart.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Bool,
|
||||||
|
Integer,
|
||||||
|
Sequence,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedSet,
|
||||||
|
NestedBool,
|
||||||
|
NestedInteger,
|
||||||
|
NestedMinMax,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .descriptors import (
|
||||||
|
NestedGapAmount,
|
||||||
|
NestedOverlap,
|
||||||
|
)
|
||||||
|
from ._chart import ChartBase
|
||||||
|
from ._3d import _3DBase
|
||||||
|
from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .series import Series
|
||||||
|
from .legend import Legend
|
||||||
|
from .label import DataLabelList
|
||||||
|
|
||||||
|
|
||||||
|
class _BarChartBase(ChartBase):
|
||||||
|
|
||||||
|
barDir = NestedSet(values=(['bar', 'col']))
|
||||||
|
type = Alias("barDir")
|
||||||
|
grouping = NestedSet(values=(['percentStacked', 'clustered', 'standard',
|
||||||
|
'stacked']))
|
||||||
|
varyColors = NestedBool(nested=True, allow_none=True)
|
||||||
|
ser = Sequence(expected_type=Series, allow_none=True)
|
||||||
|
dLbls = Typed(expected_type=DataLabelList, allow_none=True)
|
||||||
|
dataLabels = Alias("dLbls")
|
||||||
|
|
||||||
|
__elements__ = ('barDir', 'grouping', 'varyColors', 'ser', 'dLbls')
|
||||||
|
|
||||||
|
_series_type = "bar"
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
barDir="col",
|
||||||
|
grouping="clustered",
|
||||||
|
varyColors=None,
|
||||||
|
ser=(),
|
||||||
|
dLbls=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.barDir = barDir
|
||||||
|
self.grouping = grouping
|
||||||
|
self.varyColors = varyColors
|
||||||
|
self.ser = ser
|
||||||
|
self.dLbls = dLbls
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class BarChart(_BarChartBase):
|
||||||
|
|
||||||
|
tagname = "barChart"
|
||||||
|
|
||||||
|
barDir = _BarChartBase.barDir
|
||||||
|
grouping = _BarChartBase.grouping
|
||||||
|
varyColors = _BarChartBase.varyColors
|
||||||
|
ser = _BarChartBase.ser
|
||||||
|
dLbls = _BarChartBase.dLbls
|
||||||
|
|
||||||
|
gapWidth = NestedGapAmount()
|
||||||
|
overlap = NestedOverlap()
|
||||||
|
serLines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
# chart properties actually used by containing classes
|
||||||
|
x_axis = Typed(expected_type=TextAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
|
||||||
|
__elements__ = _BarChartBase.__elements__ + ('gapWidth', 'overlap', 'serLines', 'axId')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
gapWidth=150,
|
||||||
|
overlap=None,
|
||||||
|
serLines=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.gapWidth = gapWidth
|
||||||
|
self.overlap = overlap
|
||||||
|
self.serLines = serLines
|
||||||
|
self.x_axis = TextAxis()
|
||||||
|
self.y_axis = NumericAxis()
|
||||||
|
self.legend = Legend()
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class BarChart3D(_BarChartBase, _3DBase):
|
||||||
|
|
||||||
|
tagname = "bar3DChart"
|
||||||
|
|
||||||
|
barDir = _BarChartBase.barDir
|
||||||
|
grouping = _BarChartBase.grouping
|
||||||
|
varyColors = _BarChartBase.varyColors
|
||||||
|
ser = _BarChartBase.ser
|
||||||
|
dLbls = _BarChartBase.dLbls
|
||||||
|
|
||||||
|
view3D = _3DBase.view3D
|
||||||
|
floor = _3DBase.floor
|
||||||
|
sideWall = _3DBase.sideWall
|
||||||
|
backWall = _3DBase.backWall
|
||||||
|
|
||||||
|
gapWidth = NestedGapAmount()
|
||||||
|
gapDepth = NestedGapAmount()
|
||||||
|
shape = NestedNoneSet(values=(['cone', 'coneToMax', 'box', 'cylinder', 'pyramid', 'pyramidToMax']))
|
||||||
|
serLines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
x_axis = Typed(expected_type=TextAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
z_axis = Typed(expected_type=SeriesAxis, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _BarChartBase.__elements__ + ('gapWidth', 'gapDepth', 'shape', 'serLines', 'axId')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
gapWidth=150,
|
||||||
|
gapDepth=150,
|
||||||
|
shape=None,
|
||||||
|
serLines=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.gapWidth = gapWidth
|
||||||
|
self.gapDepth = gapDepth
|
||||||
|
self.shape = shape
|
||||||
|
self.serLines = serLines
|
||||||
|
self.x_axis = TextAxis()
|
||||||
|
self.y_axis = NumericAxis()
|
||||||
|
self.z_axis = SeriesAxis()
|
||||||
|
|
||||||
|
super(BarChart3D, self).__init__(**kw)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#Autogenerated schema
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Set,
|
||||||
|
MinMax,
|
||||||
|
Bool,
|
||||||
|
Integer,
|
||||||
|
Alias,
|
||||||
|
Sequence,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedMinMax,
|
||||||
|
NestedBool,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ._chart import ChartBase
|
||||||
|
from .axis import TextAxis, NumericAxis
|
||||||
|
from .series import XYSeries
|
||||||
|
from .label import DataLabelList
|
||||||
|
|
||||||
|
|
||||||
|
class BubbleChart(ChartBase):
|
||||||
|
|
||||||
|
tagname = "bubbleChart"
|
||||||
|
|
||||||
|
varyColors = NestedBool(allow_none=True)
|
||||||
|
ser = Sequence(expected_type=XYSeries, allow_none=True)
|
||||||
|
dLbls = Typed(expected_type=DataLabelList, allow_none=True)
|
||||||
|
dataLabels = Alias("dLbls")
|
||||||
|
bubble3D = NestedBool(allow_none=True)
|
||||||
|
bubbleScale = NestedMinMax(min=0, max=300, allow_none=True)
|
||||||
|
showNegBubbles = NestedBool(allow_none=True)
|
||||||
|
sizeRepresents = NestedNoneSet(values=(['area', 'w']))
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
x_axis = Typed(expected_type=NumericAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
|
||||||
|
_series_type = "bubble"
|
||||||
|
|
||||||
|
__elements__ = ('varyColors', 'ser', 'dLbls', 'bubble3D', 'bubbleScale',
|
||||||
|
'showNegBubbles', 'sizeRepresents', 'axId')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
varyColors=None,
|
||||||
|
ser=(),
|
||||||
|
dLbls=None,
|
||||||
|
bubble3D=None,
|
||||||
|
bubbleScale=None,
|
||||||
|
showNegBubbles=None,
|
||||||
|
sizeRepresents=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.varyColors = varyColors
|
||||||
|
self.ser = ser
|
||||||
|
self.dLbls = dLbls
|
||||||
|
self.bubble3D = bubble3D
|
||||||
|
self.bubbleScale = bubbleScale
|
||||||
|
self.showNegBubbles = showNegBubbles
|
||||||
|
self.sizeRepresents = sizeRepresents
|
||||||
|
self.x_axis = NumericAxis(axId=10, crossAx=20)
|
||||||
|
self.y_axis = NumericAxis(axId=20, crossAx=10)
|
||||||
|
super().__init__(**kw)
|
||||||
195
venv/lib/python3.12/site-packages/openpyxl/chart/chartspace.py
Normal file
195
venv/lib/python3.12/site-packages/openpyxl/chart/chartspace.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
|
||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
"""
|
||||||
|
Enclosing chart object. The various chart types are actually child objects.
|
||||||
|
Will probably need to call this indirectly
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
String,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import (
|
||||||
|
ExtensionList,
|
||||||
|
Relation
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedString,
|
||||||
|
NestedMinMax,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.sequence import NestedSequence
|
||||||
|
from openpyxl.xml.constants import CHART_NS
|
||||||
|
|
||||||
|
from openpyxl.drawing.colors import ColorMapping
|
||||||
|
from .text import RichText
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .legend import Legend
|
||||||
|
from ._3d import _3DBase
|
||||||
|
from .plotarea import PlotArea
|
||||||
|
from .title import Title
|
||||||
|
from .pivot import (
|
||||||
|
PivotFormat,
|
||||||
|
PivotSource,
|
||||||
|
)
|
||||||
|
from .print_settings import PrintSettings
|
||||||
|
|
||||||
|
|
||||||
|
class ChartContainer(Serialisable):
|
||||||
|
|
||||||
|
tagname = "chart"
|
||||||
|
|
||||||
|
title = Typed(expected_type=Title, allow_none=True)
|
||||||
|
autoTitleDeleted = NestedBool(allow_none=True)
|
||||||
|
pivotFmts = NestedSequence(expected_type=PivotFormat)
|
||||||
|
view3D = _3DBase.view3D
|
||||||
|
floor = _3DBase.floor
|
||||||
|
sideWall = _3DBase.sideWall
|
||||||
|
backWall = _3DBase.backWall
|
||||||
|
plotArea = Typed(expected_type=PlotArea, )
|
||||||
|
legend = Typed(expected_type=Legend, allow_none=True)
|
||||||
|
plotVisOnly = NestedBool()
|
||||||
|
dispBlanksAs = NestedNoneSet(values=(['span', 'gap', 'zero']))
|
||||||
|
showDLblsOverMax = NestedBool(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('title', 'autoTitleDeleted', 'pivotFmts', 'view3D',
|
||||||
|
'floor', 'sideWall', 'backWall', 'plotArea', 'legend', 'plotVisOnly',
|
||||||
|
'dispBlanksAs', 'showDLblsOverMax')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
title=None,
|
||||||
|
autoTitleDeleted=None,
|
||||||
|
pivotFmts=(),
|
||||||
|
view3D=None,
|
||||||
|
floor=None,
|
||||||
|
sideWall=None,
|
||||||
|
backWall=None,
|
||||||
|
plotArea=None,
|
||||||
|
legend=None,
|
||||||
|
plotVisOnly=True,
|
||||||
|
dispBlanksAs="gap",
|
||||||
|
showDLblsOverMax=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.title = title
|
||||||
|
self.autoTitleDeleted = autoTitleDeleted
|
||||||
|
self.pivotFmts = pivotFmts
|
||||||
|
self.view3D = view3D
|
||||||
|
self.floor = floor
|
||||||
|
self.sideWall = sideWall
|
||||||
|
self.backWall = backWall
|
||||||
|
if plotArea is None:
|
||||||
|
plotArea = PlotArea()
|
||||||
|
self.plotArea = plotArea
|
||||||
|
self.legend = legend
|
||||||
|
self.plotVisOnly = plotVisOnly
|
||||||
|
self.dispBlanksAs = dispBlanksAs
|
||||||
|
self.showDLblsOverMax = showDLblsOverMax
|
||||||
|
|
||||||
|
|
||||||
|
class Protection(Serialisable):
|
||||||
|
|
||||||
|
tagname = "protection"
|
||||||
|
|
||||||
|
chartObject = NestedBool(allow_none=True)
|
||||||
|
data = NestedBool(allow_none=True)
|
||||||
|
formatting = NestedBool(allow_none=True)
|
||||||
|
selection = NestedBool(allow_none=True)
|
||||||
|
userInterface = NestedBool(allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ("chartObject", "data", "formatting", "selection", "userInterface")
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
chartObject=None,
|
||||||
|
data=None,
|
||||||
|
formatting=None,
|
||||||
|
selection=None,
|
||||||
|
userInterface=None,
|
||||||
|
):
|
||||||
|
self.chartObject = chartObject
|
||||||
|
self.data = data
|
||||||
|
self.formatting = formatting
|
||||||
|
self.selection = selection
|
||||||
|
self.userInterface = userInterface
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalData(Serialisable):
|
||||||
|
|
||||||
|
tagname = "externalData"
|
||||||
|
|
||||||
|
autoUpdate = NestedBool(allow_none=True)
|
||||||
|
id = String() # Needs namespace
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
autoUpdate=None,
|
||||||
|
id=None
|
||||||
|
):
|
||||||
|
self.autoUpdate = autoUpdate
|
||||||
|
self.id = id
|
||||||
|
|
||||||
|
|
||||||
|
class ChartSpace(Serialisable):
|
||||||
|
|
||||||
|
tagname = "chartSpace"
|
||||||
|
|
||||||
|
date1904 = NestedBool(allow_none=True)
|
||||||
|
lang = NestedString(allow_none=True)
|
||||||
|
roundedCorners = NestedBool(allow_none=True)
|
||||||
|
style = NestedMinMax(allow_none=True, min=1, max=48)
|
||||||
|
clrMapOvr = Typed(expected_type=ColorMapping, allow_none=True)
|
||||||
|
pivotSource = Typed(expected_type=PivotSource, allow_none=True)
|
||||||
|
protection = Typed(expected_type=Protection, allow_none=True)
|
||||||
|
chart = Typed(expected_type=ChartContainer)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphical_properties = Alias("spPr")
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
textProperties = Alias("txPr")
|
||||||
|
externalData = Typed(expected_type=ExternalData, allow_none=True)
|
||||||
|
printSettings = Typed(expected_type=PrintSettings, allow_none=True)
|
||||||
|
userShapes = Relation()
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('date1904', 'lang', 'roundedCorners', 'style',
|
||||||
|
'clrMapOvr', 'pivotSource', 'protection', 'chart', 'spPr', 'txPr',
|
||||||
|
'externalData', 'printSettings', 'userShapes')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
date1904=None,
|
||||||
|
lang=None,
|
||||||
|
roundedCorners=None,
|
||||||
|
style=None,
|
||||||
|
clrMapOvr=None,
|
||||||
|
pivotSource=None,
|
||||||
|
protection=None,
|
||||||
|
chart=None,
|
||||||
|
spPr=None,
|
||||||
|
txPr=None,
|
||||||
|
externalData=None,
|
||||||
|
printSettings=None,
|
||||||
|
userShapes=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.date1904 = date1904
|
||||||
|
self.lang = lang
|
||||||
|
self.roundedCorners = roundedCorners
|
||||||
|
self.style = style
|
||||||
|
self.clrMapOvr = clrMapOvr
|
||||||
|
self.pivotSource = pivotSource
|
||||||
|
self.protection = protection
|
||||||
|
self.chart = chart
|
||||||
|
self.spPr = spPr
|
||||||
|
self.txPr = txPr
|
||||||
|
self.externalData = externalData
|
||||||
|
self.printSettings = printSettings
|
||||||
|
self.userShapes = userShapes
|
||||||
|
|
||||||
|
|
||||||
|
def to_tree(self, tagname=None, idx=None, namespace=None):
|
||||||
|
tree = super().to_tree()
|
||||||
|
tree.set("xmlns", CHART_NS)
|
||||||
|
return tree
|
||||||
246
venv/lib/python3.12/site-packages/openpyxl/chart/data_source.py
Normal file
246
venv/lib/python3.12/site-packages/openpyxl/chart/data_source.py
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
"""
|
||||||
|
Collection of utility primitives for charts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Bool,
|
||||||
|
Typed,
|
||||||
|
Alias,
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
Sequence,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedString,
|
||||||
|
NestedText,
|
||||||
|
NestedInteger,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NumFmt(Serialisable):
|
||||||
|
|
||||||
|
formatCode = String()
|
||||||
|
sourceLinked = Bool()
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
formatCode=None,
|
||||||
|
sourceLinked=False
|
||||||
|
):
|
||||||
|
self.formatCode = formatCode
|
||||||
|
self.sourceLinked = sourceLinked
|
||||||
|
|
||||||
|
|
||||||
|
class NumberValueDescriptor(NestedText):
|
||||||
|
"""
|
||||||
|
Data should be numerical but isn't always :-/
|
||||||
|
"""
|
||||||
|
|
||||||
|
allow_none = True
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
if value == "#N/A":
|
||||||
|
self.expected_type = str
|
||||||
|
else:
|
||||||
|
self.expected_type = float
|
||||||
|
super().__set__(instance, value)
|
||||||
|
|
||||||
|
|
||||||
|
class NumVal(Serialisable):
|
||||||
|
|
||||||
|
idx = Integer()
|
||||||
|
formatCode = NestedText(allow_none=True, expected_type=str)
|
||||||
|
v = NumberValueDescriptor()
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
idx=None,
|
||||||
|
formatCode=None,
|
||||||
|
v=None,
|
||||||
|
):
|
||||||
|
self.idx = idx
|
||||||
|
self.formatCode = formatCode
|
||||||
|
self.v = v
|
||||||
|
|
||||||
|
|
||||||
|
class NumData(Serialisable):
|
||||||
|
|
||||||
|
formatCode = NestedText(expected_type=str, allow_none=True)
|
||||||
|
ptCount = NestedInteger(allow_none=True)
|
||||||
|
pt = Sequence(expected_type=NumVal)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('formatCode', 'ptCount', 'pt')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
formatCode=None,
|
||||||
|
ptCount=None,
|
||||||
|
pt=(),
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.formatCode = formatCode
|
||||||
|
self.ptCount = ptCount
|
||||||
|
self.pt = pt
|
||||||
|
|
||||||
|
|
||||||
|
class NumRef(Serialisable):
|
||||||
|
|
||||||
|
f = NestedText(expected_type=str)
|
||||||
|
ref = Alias('f')
|
||||||
|
numCache = Typed(expected_type=NumData, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('f', 'numCache')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
f=None,
|
||||||
|
numCache=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.f = f
|
||||||
|
self.numCache = numCache
|
||||||
|
|
||||||
|
|
||||||
|
class StrVal(Serialisable):
|
||||||
|
|
||||||
|
tagname = "strVal"
|
||||||
|
|
||||||
|
idx = Integer()
|
||||||
|
v = NestedText(expected_type=str)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
idx=0,
|
||||||
|
v=None,
|
||||||
|
):
|
||||||
|
self.idx = idx
|
||||||
|
self.v = v
|
||||||
|
|
||||||
|
|
||||||
|
class StrData(Serialisable):
|
||||||
|
|
||||||
|
tagname = "strData"
|
||||||
|
|
||||||
|
ptCount = NestedInteger(allow_none=True)
|
||||||
|
pt = Sequence(expected_type=StrVal)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('ptCount', 'pt')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
ptCount=None,
|
||||||
|
pt=(),
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.ptCount = ptCount
|
||||||
|
self.pt = pt
|
||||||
|
|
||||||
|
|
||||||
|
class StrRef(Serialisable):
|
||||||
|
|
||||||
|
tagname = "strRef"
|
||||||
|
|
||||||
|
f = NestedText(expected_type=str, allow_none=True)
|
||||||
|
strCache = Typed(expected_type=StrData, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('f', 'strCache')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
f=None,
|
||||||
|
strCache=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.f = f
|
||||||
|
self.strCache = strCache
|
||||||
|
|
||||||
|
|
||||||
|
class NumDataSource(Serialisable):
|
||||||
|
|
||||||
|
numRef = Typed(expected_type=NumRef, allow_none=True)
|
||||||
|
numLit = Typed(expected_type=NumData, allow_none=True)
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
numRef=None,
|
||||||
|
numLit=None,
|
||||||
|
):
|
||||||
|
self.numRef = numRef
|
||||||
|
self.numLit = numLit
|
||||||
|
|
||||||
|
|
||||||
|
class Level(Serialisable):
|
||||||
|
|
||||||
|
tagname = "lvl"
|
||||||
|
|
||||||
|
pt = Sequence(expected_type=StrVal)
|
||||||
|
|
||||||
|
__elements__ = ('pt',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
pt=(),
|
||||||
|
):
|
||||||
|
self.pt = pt
|
||||||
|
|
||||||
|
|
||||||
|
class MultiLevelStrData(Serialisable):
|
||||||
|
|
||||||
|
tagname = "multiLvlStrData"
|
||||||
|
|
||||||
|
ptCount = Integer(allow_none=True)
|
||||||
|
lvl = Sequence(expected_type=Level)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('ptCount', 'lvl',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
ptCount=None,
|
||||||
|
lvl=(),
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.ptCount = ptCount
|
||||||
|
self.lvl = lvl
|
||||||
|
|
||||||
|
|
||||||
|
class MultiLevelStrRef(Serialisable):
|
||||||
|
|
||||||
|
tagname = "multiLvlStrRef"
|
||||||
|
|
||||||
|
f = NestedText(expected_type=str)
|
||||||
|
multiLvlStrCache = Typed(expected_type=MultiLevelStrData, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('multiLvlStrCache', 'f')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
f=None,
|
||||||
|
multiLvlStrCache=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.f = f
|
||||||
|
self.multiLvlStrCache = multiLvlStrCache
|
||||||
|
|
||||||
|
|
||||||
|
class AxDataSource(Serialisable):
|
||||||
|
|
||||||
|
tagname = "cat"
|
||||||
|
|
||||||
|
numRef = Typed(expected_type=NumRef, allow_none=True)
|
||||||
|
numLit = Typed(expected_type=NumData, allow_none=True)
|
||||||
|
strRef = Typed(expected_type=StrRef, allow_none=True)
|
||||||
|
strLit = Typed(expected_type=StrData, allow_none=True)
|
||||||
|
multiLvlStrRef = Typed(expected_type=MultiLevelStrRef, allow_none=True)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
numRef=None,
|
||||||
|
numLit=None,
|
||||||
|
strRef=None,
|
||||||
|
strLit=None,
|
||||||
|
multiLvlStrRef=None,
|
||||||
|
):
|
||||||
|
if not any([numLit, numRef, strRef, strLit, multiLvlStrRef]):
|
||||||
|
raise TypeError("A data source must be provided")
|
||||||
|
self.numRef = numRef
|
||||||
|
self.numLit = numLit
|
||||||
|
self.strRef = strRef
|
||||||
|
self.strLit = strLit
|
||||||
|
self.multiLvlStrRef = multiLvlStrRef
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedMinMax
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.descriptors import Typed
|
||||||
|
|
||||||
|
from .data_source import NumFmt
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utility descriptors for the chart module.
|
||||||
|
For convenience but also clarity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class NestedGapAmount(NestedMinMax):
|
||||||
|
|
||||||
|
allow_none = True
|
||||||
|
min = 0
|
||||||
|
max = 500
|
||||||
|
|
||||||
|
|
||||||
|
class NestedOverlap(NestedMinMax):
|
||||||
|
|
||||||
|
allow_none = True
|
||||||
|
min = -100
|
||||||
|
max = 100
|
||||||
|
|
||||||
|
|
||||||
|
class NumberFormatDescriptor(Typed):
|
||||||
|
"""
|
||||||
|
Allow direct assignment of format code
|
||||||
|
"""
|
||||||
|
|
||||||
|
expected_type = NumFmt
|
||||||
|
allow_none = True
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = NumFmt(value)
|
||||||
|
super().__set__(instance, value)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Float,
|
||||||
|
Set,
|
||||||
|
Alias
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedSet,
|
||||||
|
NestedBool,
|
||||||
|
NestedFloat,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .data_source import NumDataSource
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorBars(Serialisable):
|
||||||
|
|
||||||
|
tagname = "errBars"
|
||||||
|
|
||||||
|
errDir = NestedNoneSet(values=(['x', 'y']))
|
||||||
|
direction = Alias("errDir")
|
||||||
|
errBarType = NestedSet(values=(['both', 'minus', 'plus']))
|
||||||
|
style = Alias("errBarType")
|
||||||
|
errValType = NestedSet(values=(['cust', 'fixedVal', 'percentage', 'stdDev', 'stdErr']))
|
||||||
|
size = Alias("errValType")
|
||||||
|
noEndCap = NestedBool(nested=True, allow_none=True)
|
||||||
|
plus = Typed(expected_type=NumDataSource, allow_none=True)
|
||||||
|
minus = Typed(expected_type=NumDataSource, allow_none=True)
|
||||||
|
val = NestedFloat(allow_none=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias("spPr")
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('errDir','errBarType', 'errValType', 'noEndCap','minus', 'plus', 'val', 'spPr')
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
errDir=None,
|
||||||
|
errBarType="both",
|
||||||
|
errValType="fixedVal",
|
||||||
|
noEndCap=None,
|
||||||
|
plus=None,
|
||||||
|
minus=None,
|
||||||
|
val=None,
|
||||||
|
spPr=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.errDir = errDir
|
||||||
|
self.errBarType = errBarType
|
||||||
|
self.errValType = errValType
|
||||||
|
self.noEndCap = noEndCap
|
||||||
|
self.plus = plus
|
||||||
|
self.minus = minus
|
||||||
|
self.val = val
|
||||||
|
self.spPr = spPr
|
||||||
127
venv/lib/python3.12/site-packages/openpyxl/chart/label.py
Normal file
127
venv/lib/python3.12/site-packages/openpyxl/chart/label.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Sequence,
|
||||||
|
Alias,
|
||||||
|
Typed
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedBool,
|
||||||
|
NestedString,
|
||||||
|
NestedInteger,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .text import RichText
|
||||||
|
|
||||||
|
|
||||||
|
class _DataLabelBase(Serialisable):
|
||||||
|
|
||||||
|
numFmt = NestedString(allow_none=True, attribute="formatCode")
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
textProperties = Alias('txPr')
|
||||||
|
dLblPos = NestedNoneSet(values=['bestFit', 'b', 'ctr', 'inBase', 'inEnd',
|
||||||
|
'l', 'outEnd', 'r', 't'])
|
||||||
|
position = Alias('dLblPos')
|
||||||
|
showLegendKey = NestedBool(allow_none=True)
|
||||||
|
showVal = NestedBool(allow_none=True)
|
||||||
|
showCatName = NestedBool(allow_none=True)
|
||||||
|
showSerName = NestedBool(allow_none=True)
|
||||||
|
showPercent = NestedBool(allow_none=True)
|
||||||
|
showBubbleSize = NestedBool(allow_none=True)
|
||||||
|
showLeaderLines = NestedBool(allow_none=True)
|
||||||
|
separator = NestedString(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ("numFmt", "spPr", "txPr", "dLblPos", "showLegendKey",
|
||||||
|
"showVal", "showCatName", "showSerName", "showPercent", "showBubbleSize",
|
||||||
|
"showLeaderLines", "separator")
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
numFmt=None,
|
||||||
|
spPr=None,
|
||||||
|
txPr=None,
|
||||||
|
dLblPos=None,
|
||||||
|
showLegendKey=None,
|
||||||
|
showVal=None,
|
||||||
|
showCatName=None,
|
||||||
|
showSerName=None,
|
||||||
|
showPercent=None,
|
||||||
|
showBubbleSize=None,
|
||||||
|
showLeaderLines=None,
|
||||||
|
separator=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.numFmt = numFmt
|
||||||
|
self.spPr = spPr
|
||||||
|
self.txPr = txPr
|
||||||
|
self.dLblPos = dLblPos
|
||||||
|
self.showLegendKey = showLegendKey
|
||||||
|
self.showVal = showVal
|
||||||
|
self.showCatName = showCatName
|
||||||
|
self.showSerName = showSerName
|
||||||
|
self.showPercent = showPercent
|
||||||
|
self.showBubbleSize = showBubbleSize
|
||||||
|
self.showLeaderLines = showLeaderLines
|
||||||
|
self.separator = separator
|
||||||
|
|
||||||
|
|
||||||
|
class DataLabel(_DataLabelBase):
|
||||||
|
|
||||||
|
tagname = "dLbl"
|
||||||
|
|
||||||
|
idx = NestedInteger()
|
||||||
|
|
||||||
|
numFmt = _DataLabelBase.numFmt
|
||||||
|
spPr = _DataLabelBase.spPr
|
||||||
|
txPr = _DataLabelBase.txPr
|
||||||
|
dLblPos = _DataLabelBase.dLblPos
|
||||||
|
showLegendKey = _DataLabelBase.showLegendKey
|
||||||
|
showVal = _DataLabelBase.showVal
|
||||||
|
showCatName = _DataLabelBase.showCatName
|
||||||
|
showSerName = _DataLabelBase.showSerName
|
||||||
|
showPercent = _DataLabelBase.showPercent
|
||||||
|
showBubbleSize = _DataLabelBase.showBubbleSize
|
||||||
|
showLeaderLines = _DataLabelBase.showLeaderLines
|
||||||
|
separator = _DataLabelBase.separator
|
||||||
|
extLst = _DataLabelBase.extLst
|
||||||
|
|
||||||
|
__elements__ = ("idx",) + _DataLabelBase.__elements__
|
||||||
|
|
||||||
|
def __init__(self, idx=0, **kw ):
|
||||||
|
self.idx = idx
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class DataLabelList(_DataLabelBase):
|
||||||
|
|
||||||
|
tagname = "dLbls"
|
||||||
|
|
||||||
|
dLbl = Sequence(expected_type=DataLabel, allow_none=True)
|
||||||
|
|
||||||
|
delete = NestedBool(allow_none=True)
|
||||||
|
numFmt = _DataLabelBase.numFmt
|
||||||
|
spPr = _DataLabelBase.spPr
|
||||||
|
txPr = _DataLabelBase.txPr
|
||||||
|
dLblPos = _DataLabelBase.dLblPos
|
||||||
|
showLegendKey = _DataLabelBase.showLegendKey
|
||||||
|
showVal = _DataLabelBase.showVal
|
||||||
|
showCatName = _DataLabelBase.showCatName
|
||||||
|
showSerName = _DataLabelBase.showSerName
|
||||||
|
showPercent = _DataLabelBase.showPercent
|
||||||
|
showBubbleSize = _DataLabelBase.showBubbleSize
|
||||||
|
showLeaderLines = _DataLabelBase.showLeaderLines
|
||||||
|
separator = _DataLabelBase.separator
|
||||||
|
extLst = _DataLabelBase.extLst
|
||||||
|
|
||||||
|
__elements__ = ("delete", "dLbl",) + _DataLabelBase.__elements__
|
||||||
|
|
||||||
|
def __init__(self, dLbl=(), delete=None, **kw):
|
||||||
|
self.dLbl = dLbl
|
||||||
|
self.delete = delete
|
||||||
|
super().__init__(**kw)
|
||||||
74
venv/lib/python3.12/site-packages/openpyxl/chart/layout.py
Normal file
74
venv/lib/python3.12/site-packages/openpyxl/chart/layout.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
NoneSet,
|
||||||
|
Float,
|
||||||
|
Typed,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedSet,
|
||||||
|
NestedMinMax,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ManualLayout(Serialisable):
|
||||||
|
|
||||||
|
tagname = "manualLayout"
|
||||||
|
|
||||||
|
layoutTarget = NestedNoneSet(values=(['inner', 'outer']))
|
||||||
|
xMode = NestedNoneSet(values=(['edge', 'factor']))
|
||||||
|
yMode = NestedNoneSet(values=(['edge', 'factor']))
|
||||||
|
wMode = NestedSet(values=(['edge', 'factor']))
|
||||||
|
hMode = NestedSet(values=(['edge', 'factor']))
|
||||||
|
x = NestedMinMax(min=-1, max=1, allow_none=True)
|
||||||
|
y = NestedMinMax(min=-1, max=1, allow_none=True)
|
||||||
|
w = NestedMinMax(min=0, max=1, allow_none=True)
|
||||||
|
width = Alias('w')
|
||||||
|
h = NestedMinMax(min=0, max=1, allow_none=True)
|
||||||
|
height = Alias('h')
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('layoutTarget', 'xMode', 'yMode', 'wMode', 'hMode', 'x',
|
||||||
|
'y', 'w', 'h')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
layoutTarget=None,
|
||||||
|
xMode=None,
|
||||||
|
yMode=None,
|
||||||
|
wMode="factor",
|
||||||
|
hMode="factor",
|
||||||
|
x=None,
|
||||||
|
y=None,
|
||||||
|
w=None,
|
||||||
|
h=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.layoutTarget = layoutTarget
|
||||||
|
self.xMode = xMode
|
||||||
|
self.yMode = yMode
|
||||||
|
self.wMode = wMode
|
||||||
|
self.hMode = hMode
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.w = w
|
||||||
|
self.h = h
|
||||||
|
|
||||||
|
|
||||||
|
class Layout(Serialisable):
|
||||||
|
|
||||||
|
tagname = "layout"
|
||||||
|
|
||||||
|
manualLayout = Typed(expected_type=ManualLayout, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('manualLayout',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
manualLayout=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.manualLayout = manualLayout
|
||||||
75
venv/lib/python3.12/site-packages/openpyxl/chart/legend.py
Normal file
75
venv/lib/python3.12/site-packages/openpyxl/chart/legend.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Integer,
|
||||||
|
Alias,
|
||||||
|
Sequence,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedSet,
|
||||||
|
NestedInteger
|
||||||
|
)
|
||||||
|
|
||||||
|
from .layout import Layout
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .text import RichText
|
||||||
|
|
||||||
|
|
||||||
|
class LegendEntry(Serialisable):
|
||||||
|
|
||||||
|
tagname = "legendEntry"
|
||||||
|
|
||||||
|
idx = NestedInteger()
|
||||||
|
delete = NestedBool()
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('idx', 'delete', 'txPr')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
idx=0,
|
||||||
|
delete=False,
|
||||||
|
txPr=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.idx = idx
|
||||||
|
self.delete = delete
|
||||||
|
self.txPr = txPr
|
||||||
|
|
||||||
|
|
||||||
|
class Legend(Serialisable):
|
||||||
|
|
||||||
|
tagname = "legend"
|
||||||
|
|
||||||
|
legendPos = NestedSet(values=(['b', 'tr', 'l', 'r', 't']))
|
||||||
|
position = Alias('legendPos')
|
||||||
|
legendEntry = Sequence(expected_type=LegendEntry)
|
||||||
|
layout = Typed(expected_type=Layout, allow_none=True)
|
||||||
|
overlay = NestedBool(allow_none=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
textProperties = Alias('txPr')
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('legendPos', 'legendEntry', 'layout', 'overlay', 'spPr', 'txPr',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
legendPos="r",
|
||||||
|
legendEntry=(),
|
||||||
|
layout=None,
|
||||||
|
overlay=None,
|
||||||
|
spPr=None,
|
||||||
|
txPr=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.legendPos = legendPos
|
||||||
|
self.legendEntry = legendEntry
|
||||||
|
self.layout = layout
|
||||||
|
self.overlay = overlay
|
||||||
|
self.spPr = spPr
|
||||||
|
self.txPr = txPr
|
||||||
129
venv/lib/python3.12/site-packages/openpyxl/chart/line_chart.py
Normal file
129
venv/lib/python3.12/site-packages/openpyxl/chart/line_chart.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#Autogenerated schema
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Sequence,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedSet,
|
||||||
|
NestedBool,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ._chart import ChartBase
|
||||||
|
from .updown_bars import UpDownBars
|
||||||
|
from .descriptors import NestedGapAmount
|
||||||
|
from .axis import TextAxis, NumericAxis, SeriesAxis, ChartLines, _BaseAxis
|
||||||
|
from .label import DataLabelList
|
||||||
|
from .series import Series
|
||||||
|
|
||||||
|
|
||||||
|
class _LineChartBase(ChartBase):
|
||||||
|
|
||||||
|
grouping = NestedSet(values=(['percentStacked', 'standard', 'stacked']))
|
||||||
|
varyColors = NestedBool(allow_none=True)
|
||||||
|
ser = Sequence(expected_type=Series, allow_none=True)
|
||||||
|
dLbls = Typed(expected_type=DataLabelList, allow_none=True)
|
||||||
|
dataLabels = Alias("dLbls")
|
||||||
|
dropLines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
|
||||||
|
_series_type = "line"
|
||||||
|
|
||||||
|
__elements__ = ('grouping', 'varyColors', 'ser', 'dLbls', 'dropLines')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
grouping="standard",
|
||||||
|
varyColors=None,
|
||||||
|
ser=(),
|
||||||
|
dLbls=None,
|
||||||
|
dropLines=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.grouping = grouping
|
||||||
|
self.varyColors = varyColors
|
||||||
|
self.ser = ser
|
||||||
|
self.dLbls = dLbls
|
||||||
|
self.dropLines = dropLines
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class LineChart(_LineChartBase):
|
||||||
|
|
||||||
|
tagname = "lineChart"
|
||||||
|
|
||||||
|
grouping = _LineChartBase.grouping
|
||||||
|
varyColors = _LineChartBase.varyColors
|
||||||
|
ser = _LineChartBase.ser
|
||||||
|
dLbls = _LineChartBase.dLbls
|
||||||
|
dropLines =_LineChartBase.dropLines
|
||||||
|
|
||||||
|
hiLowLines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
upDownBars = Typed(expected_type=UpDownBars, allow_none=True)
|
||||||
|
marker = NestedBool(allow_none=True)
|
||||||
|
smooth = NestedBool(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
x_axis = Typed(expected_type=_BaseAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
|
||||||
|
__elements__ = _LineChartBase.__elements__ + ('hiLowLines', 'upDownBars', 'marker', 'smooth', 'axId')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
hiLowLines=None,
|
||||||
|
upDownBars=None,
|
||||||
|
marker=None,
|
||||||
|
smooth=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.hiLowLines = hiLowLines
|
||||||
|
self.upDownBars = upDownBars
|
||||||
|
self.marker = marker
|
||||||
|
self.smooth = smooth
|
||||||
|
self.x_axis = TextAxis()
|
||||||
|
self.y_axis = NumericAxis()
|
||||||
|
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class LineChart3D(_LineChartBase):
|
||||||
|
|
||||||
|
tagname = "line3DChart"
|
||||||
|
|
||||||
|
grouping = _LineChartBase.grouping
|
||||||
|
varyColors = _LineChartBase.varyColors
|
||||||
|
ser = _LineChartBase.ser
|
||||||
|
dLbls = _LineChartBase.dLbls
|
||||||
|
dropLines =_LineChartBase.dropLines
|
||||||
|
|
||||||
|
gapDepth = NestedGapAmount()
|
||||||
|
hiLowLines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
upDownBars = Typed(expected_type=UpDownBars, allow_none=True)
|
||||||
|
marker = NestedBool(allow_none=True)
|
||||||
|
smooth = NestedBool(allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
x_axis = Typed(expected_type=TextAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
z_axis = Typed(expected_type=SeriesAxis)
|
||||||
|
|
||||||
|
__elements__ = _LineChartBase.__elements__ + ('gapDepth', 'hiLowLines',
|
||||||
|
'upDownBars', 'marker', 'smooth', 'axId')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
gapDepth=None,
|
||||||
|
hiLowLines=None,
|
||||||
|
upDownBars=None,
|
||||||
|
marker=None,
|
||||||
|
smooth=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.gapDepth = gapDepth
|
||||||
|
self.hiLowLines = hiLowLines
|
||||||
|
self.upDownBars = upDownBars
|
||||||
|
self.marker = marker
|
||||||
|
self.smooth = smooth
|
||||||
|
self.x_axis = TextAxis()
|
||||||
|
self.y_axis = NumericAxis()
|
||||||
|
self.z_axis = SeriesAxis()
|
||||||
|
super(LineChart3D, self).__init__(**kw)
|
||||||
90
venv/lib/python3.12/site-packages/openpyxl/chart/marker.py
Normal file
90
venv/lib/python3.12/site-packages/openpyxl/chart/marker.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.descriptors.excel import(
|
||||||
|
ExtensionList,
|
||||||
|
_explicit_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedInteger,
|
||||||
|
NestedMinMax,
|
||||||
|
NestedNoneSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .layout import Layout
|
||||||
|
from .picture import PictureOptions
|
||||||
|
from .shapes import *
|
||||||
|
from .text import *
|
||||||
|
from .error_bar import *
|
||||||
|
|
||||||
|
|
||||||
|
class Marker(Serialisable):
|
||||||
|
|
||||||
|
tagname = "marker"
|
||||||
|
|
||||||
|
symbol = NestedNoneSet(values=(['circle', 'dash', 'diamond', 'dot', 'picture',
|
||||||
|
'plus', 'square', 'star', 'triangle', 'x', 'auto']),
|
||||||
|
to_tree=_explicit_none)
|
||||||
|
size = NestedMinMax(min=2, max=72, allow_none=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('symbol', 'size', 'spPr')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
symbol=None,
|
||||||
|
size=None,
|
||||||
|
spPr=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.symbol = symbol
|
||||||
|
self.size = size
|
||||||
|
if spPr is None:
|
||||||
|
spPr = GraphicalProperties()
|
||||||
|
self.spPr = spPr
|
||||||
|
|
||||||
|
|
||||||
|
class DataPoint(Serialisable):
|
||||||
|
|
||||||
|
tagname = "dPt"
|
||||||
|
|
||||||
|
idx = NestedInteger()
|
||||||
|
invertIfNegative = NestedBool(allow_none=True)
|
||||||
|
marker = Typed(expected_type=Marker, allow_none=True)
|
||||||
|
bubble3D = NestedBool(allow_none=True)
|
||||||
|
explosion = NestedInteger(allow_none=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
pictureOptions = Typed(expected_type=PictureOptions, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('idx', 'invertIfNegative', 'marker', 'bubble3D',
|
||||||
|
'explosion', 'spPr', 'pictureOptions')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
idx=None,
|
||||||
|
invertIfNegative=None,
|
||||||
|
marker=None,
|
||||||
|
bubble3D=None,
|
||||||
|
explosion=None,
|
||||||
|
spPr=None,
|
||||||
|
pictureOptions=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.idx = idx
|
||||||
|
self.invertIfNegative = invertIfNegative
|
||||||
|
self.marker = marker
|
||||||
|
self.bubble3D = bubble3D
|
||||||
|
self.explosion = explosion
|
||||||
|
if spPr is None:
|
||||||
|
spPr = GraphicalProperties()
|
||||||
|
self.spPr = spPr
|
||||||
|
self.pictureOptions = pictureOptions
|
||||||
35
venv/lib/python3.12/site-packages/openpyxl/chart/picture.py
Normal file
35
venv/lib/python3.12/site-packages/openpyxl/chart/picture.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedFloat,
|
||||||
|
NestedMinMax,
|
||||||
|
NestedNoneSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
class PictureOptions(Serialisable):
|
||||||
|
|
||||||
|
tagname = "pictureOptions"
|
||||||
|
|
||||||
|
applyToFront = NestedBool(allow_none=True, nested=True)
|
||||||
|
applyToSides = NestedBool(allow_none=True, nested=True)
|
||||||
|
applyToEnd = NestedBool(allow_none=True, nested=True)
|
||||||
|
pictureFormat = NestedNoneSet(values=(['stretch', 'stack', 'stackScale']), nested=True)
|
||||||
|
pictureStackUnit = NestedFloat(allow_none=True, nested=True)
|
||||||
|
|
||||||
|
__elements__ = ('applyToFront', 'applyToSides', 'applyToEnd', 'pictureFormat', 'pictureStackUnit')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
applyToFront=None,
|
||||||
|
applyToSides=None,
|
||||||
|
applyToEnd=None,
|
||||||
|
pictureFormat=None,
|
||||||
|
pictureStackUnit=None,
|
||||||
|
):
|
||||||
|
self.applyToFront = applyToFront
|
||||||
|
self.applyToSides = applyToSides
|
||||||
|
self.applyToEnd = applyToEnd
|
||||||
|
self.pictureFormat = pictureFormat
|
||||||
|
self.pictureStackUnit = pictureStackUnit
|
||||||
177
venv/lib/python3.12/site-packages/openpyxl/chart/pie_chart.py
Normal file
177
venv/lib/python3.12/site-packages/openpyxl/chart/pie_chart.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#Autogenerated schema
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Bool,
|
||||||
|
MinMax,
|
||||||
|
Integer,
|
||||||
|
NoneSet,
|
||||||
|
Float,
|
||||||
|
Alias,
|
||||||
|
Sequence,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList, Percentage
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedMinMax,
|
||||||
|
NestedInteger,
|
||||||
|
NestedFloat,
|
||||||
|
NestedNoneSet,
|
||||||
|
NestedSet,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.sequence import ValueSequence
|
||||||
|
|
||||||
|
from ._chart import ChartBase
|
||||||
|
from .axis import ChartLines
|
||||||
|
from .descriptors import NestedGapAmount
|
||||||
|
from .series import Series
|
||||||
|
from .label import DataLabelList
|
||||||
|
|
||||||
|
|
||||||
|
class _PieChartBase(ChartBase):
|
||||||
|
|
||||||
|
varyColors = NestedBool(allow_none=True)
|
||||||
|
ser = Sequence(expected_type=Series, allow_none=True)
|
||||||
|
dLbls = Typed(expected_type=DataLabelList, allow_none=True)
|
||||||
|
dataLabels = Alias("dLbls")
|
||||||
|
|
||||||
|
_series_type = "pie"
|
||||||
|
|
||||||
|
__elements__ = ('varyColors', 'ser', 'dLbls')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
varyColors=True,
|
||||||
|
ser=(),
|
||||||
|
dLbls=None,
|
||||||
|
):
|
||||||
|
self.varyColors = varyColors
|
||||||
|
self.ser = ser
|
||||||
|
self.dLbls = dLbls
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PieChart(_PieChartBase):
|
||||||
|
|
||||||
|
tagname = "pieChart"
|
||||||
|
|
||||||
|
varyColors = _PieChartBase.varyColors
|
||||||
|
ser = _PieChartBase.ser
|
||||||
|
dLbls = _PieChartBase.dLbls
|
||||||
|
|
||||||
|
firstSliceAng = NestedMinMax(min=0, max=360)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _PieChartBase.__elements__ + ('firstSliceAng', )
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
firstSliceAng=0,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.firstSliceAng = firstSliceAng
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class PieChart3D(_PieChartBase):
|
||||||
|
|
||||||
|
tagname = "pie3DChart"
|
||||||
|
|
||||||
|
varyColors = _PieChartBase.varyColors
|
||||||
|
ser = _PieChartBase.ser
|
||||||
|
dLbls = _PieChartBase.dLbls
|
||||||
|
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _PieChartBase.__elements__
|
||||||
|
|
||||||
|
|
||||||
|
class DoughnutChart(_PieChartBase):
|
||||||
|
|
||||||
|
tagname = "doughnutChart"
|
||||||
|
|
||||||
|
varyColors = _PieChartBase.varyColors
|
||||||
|
ser = _PieChartBase.ser
|
||||||
|
dLbls = _PieChartBase.dLbls
|
||||||
|
|
||||||
|
firstSliceAng = NestedMinMax(min=0, max=360)
|
||||||
|
holeSize = NestedMinMax(min=1, max=90, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _PieChartBase.__elements__ + ('firstSliceAng', 'holeSize')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
firstSliceAng=0,
|
||||||
|
holeSize=10,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.firstSliceAng = firstSliceAng
|
||||||
|
self.holeSize = holeSize
|
||||||
|
super().__init__(**kw)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSplit(Serialisable):
|
||||||
|
|
||||||
|
tagname = "custSplit"
|
||||||
|
|
||||||
|
secondPiePt = ValueSequence(expected_type=int)
|
||||||
|
|
||||||
|
__elements__ = ('secondPiePt',)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
secondPiePt=(),
|
||||||
|
):
|
||||||
|
self.secondPiePt = secondPiePt
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectedPieChart(_PieChartBase):
|
||||||
|
|
||||||
|
"""
|
||||||
|
From the spec 21.2.2.126
|
||||||
|
|
||||||
|
This element contains the pie of pie or bar of pie series on this
|
||||||
|
chart. Only the first series shall be displayed. The splitType element
|
||||||
|
shall determine whether the splitPos and custSplit elements apply.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tagname = "ofPieChart"
|
||||||
|
|
||||||
|
varyColors = _PieChartBase.varyColors
|
||||||
|
ser = _PieChartBase.ser
|
||||||
|
dLbls = _PieChartBase.dLbls
|
||||||
|
|
||||||
|
ofPieType = NestedSet(values=(['pie', 'bar']))
|
||||||
|
type = Alias('ofPieType')
|
||||||
|
gapWidth = NestedGapAmount()
|
||||||
|
splitType = NestedNoneSet(values=(['auto', 'cust', 'percent', 'pos', 'val']))
|
||||||
|
splitPos = NestedFloat(allow_none=True)
|
||||||
|
custSplit = Typed(expected_type=CustomSplit, allow_none=True)
|
||||||
|
secondPieSize = NestedMinMax(min=5, max=200, allow_none=True)
|
||||||
|
serLines = Typed(expected_type=ChartLines, allow_none=True)
|
||||||
|
join_lines = Alias('serLines')
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = _PieChartBase.__elements__ + ('ofPieType', 'gapWidth',
|
||||||
|
'splitType', 'splitPos', 'custSplit', 'secondPieSize', 'serLines')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
ofPieType="pie",
|
||||||
|
gapWidth=None,
|
||||||
|
splitType="auto",
|
||||||
|
splitPos=None,
|
||||||
|
custSplit=None,
|
||||||
|
secondPieSize=75,
|
||||||
|
serLines=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.ofPieType = ofPieType
|
||||||
|
self.gapWidth = gapWidth
|
||||||
|
self.splitType = splitType
|
||||||
|
self.splitPos = splitPos
|
||||||
|
self.custSplit = custSplit
|
||||||
|
self.secondPieSize = secondPieSize
|
||||||
|
if serLines is None:
|
||||||
|
self.serLines = ChartLines()
|
||||||
|
super().__init__(**kw)
|
||||||
65
venv/lib/python3.12/site-packages/openpyxl/chart/pivot.py
Normal file
65
venv/lib/python3.12/site-packages/openpyxl/chart/pivot.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
|
||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Alias,
|
||||||
|
Typed,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.nested import NestedInteger, NestedText
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
|
||||||
|
from .label import DataLabel
|
||||||
|
from .marker import Marker
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .text import RichText
|
||||||
|
|
||||||
|
|
||||||
|
class PivotSource(Serialisable):
|
||||||
|
|
||||||
|
tagname = "pivotSource"
|
||||||
|
|
||||||
|
name = NestedText(expected_type=str)
|
||||||
|
fmtId = NestedInteger(expected_type=int)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('name', 'fmtId')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
name=None,
|
||||||
|
fmtId=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.fmtId = fmtId
|
||||||
|
|
||||||
|
|
||||||
|
class PivotFormat(Serialisable):
|
||||||
|
|
||||||
|
tagname = "pivotFmt"
|
||||||
|
|
||||||
|
idx = NestedInteger(nested=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias("spPr")
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
TextBody = Alias("txPr")
|
||||||
|
marker = Typed(expected_type=Marker, allow_none=True)
|
||||||
|
dLbl = Typed(expected_type=DataLabel, allow_none=True)
|
||||||
|
DataLabel = Alias("dLbl")
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('idx', 'spPr', 'txPr', 'marker', 'dLbl')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
idx=0,
|
||||||
|
spPr=None,
|
||||||
|
txPr=None,
|
||||||
|
marker=None,
|
||||||
|
dLbl=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.idx = idx
|
||||||
|
self.spPr = spPr
|
||||||
|
self.txPr = txPr
|
||||||
|
self.marker = marker
|
||||||
|
self.dLbl = dLbl
|
||||||
162
venv/lib/python3.12/site-packages/openpyxl/chart/plotarea.py
Normal file
162
venv/lib/python3.12/site-packages/openpyxl/chart/plotarea.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Typed,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import (
|
||||||
|
ExtensionList,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.sequence import (
|
||||||
|
MultiSequence,
|
||||||
|
MultiSequencePart,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ._3d import _3DBase
|
||||||
|
from .area_chart import AreaChart, AreaChart3D
|
||||||
|
from .bar_chart import BarChart, BarChart3D
|
||||||
|
from .bubble_chart import BubbleChart
|
||||||
|
from .line_chart import LineChart, LineChart3D
|
||||||
|
from .pie_chart import PieChart, PieChart3D, ProjectedPieChart, DoughnutChart
|
||||||
|
from .radar_chart import RadarChart
|
||||||
|
from .scatter_chart import ScatterChart
|
||||||
|
from .stock_chart import StockChart
|
||||||
|
from .surface_chart import SurfaceChart, SurfaceChart3D
|
||||||
|
from .layout import Layout
|
||||||
|
from .shapes import GraphicalProperties
|
||||||
|
from .text import RichText
|
||||||
|
|
||||||
|
from .axis import (
|
||||||
|
NumericAxis,
|
||||||
|
TextAxis,
|
||||||
|
SeriesAxis,
|
||||||
|
DateAxis,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DataTable(Serialisable):
|
||||||
|
|
||||||
|
tagname = "dTable"
|
||||||
|
|
||||||
|
showHorzBorder = NestedBool(allow_none=True)
|
||||||
|
showVertBorder = NestedBool(allow_none=True)
|
||||||
|
showOutline = NestedBool(allow_none=True)
|
||||||
|
showKeys = NestedBool(allow_none=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias('spPr')
|
||||||
|
txPr = Typed(expected_type=RichText, allow_none=True)
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ('showHorzBorder', 'showVertBorder', 'showOutline',
|
||||||
|
'showKeys', 'spPr', 'txPr')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
showHorzBorder=None,
|
||||||
|
showVertBorder=None,
|
||||||
|
showOutline=None,
|
||||||
|
showKeys=None,
|
||||||
|
spPr=None,
|
||||||
|
txPr=None,
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.showHorzBorder = showHorzBorder
|
||||||
|
self.showVertBorder = showVertBorder
|
||||||
|
self.showOutline = showOutline
|
||||||
|
self.showKeys = showKeys
|
||||||
|
self.spPr = spPr
|
||||||
|
self.txPr = txPr
|
||||||
|
|
||||||
|
|
||||||
|
class PlotArea(Serialisable):
|
||||||
|
|
||||||
|
tagname = "plotArea"
|
||||||
|
|
||||||
|
layout = Typed(expected_type=Layout, allow_none=True)
|
||||||
|
dTable = Typed(expected_type=DataTable, allow_none=True)
|
||||||
|
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
|
||||||
|
graphicalProperties = Alias("spPr")
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
# at least one chart
|
||||||
|
_charts = MultiSequence()
|
||||||
|
areaChart = MultiSequencePart(expected_type=AreaChart, store="_charts")
|
||||||
|
area3DChart = MultiSequencePart(expected_type=AreaChart3D, store="_charts")
|
||||||
|
lineChart = MultiSequencePart(expected_type=LineChart, store="_charts")
|
||||||
|
line3DChart = MultiSequencePart(expected_type=LineChart3D, store="_charts")
|
||||||
|
stockChart = MultiSequencePart(expected_type=StockChart, store="_charts")
|
||||||
|
radarChart = MultiSequencePart(expected_type=RadarChart, store="_charts")
|
||||||
|
scatterChart = MultiSequencePart(expected_type=ScatterChart, store="_charts")
|
||||||
|
pieChart = MultiSequencePart(expected_type=PieChart, store="_charts")
|
||||||
|
pie3DChart = MultiSequencePart(expected_type=PieChart3D, store="_charts")
|
||||||
|
doughnutChart = MultiSequencePart(expected_type=DoughnutChart, store="_charts")
|
||||||
|
barChart = MultiSequencePart(expected_type=BarChart, store="_charts")
|
||||||
|
bar3DChart = MultiSequencePart(expected_type=BarChart3D, store="_charts")
|
||||||
|
ofPieChart = MultiSequencePart(expected_type=ProjectedPieChart, store="_charts")
|
||||||
|
surfaceChart = MultiSequencePart(expected_type=SurfaceChart, store="_charts")
|
||||||
|
surface3DChart = MultiSequencePart(expected_type=SurfaceChart3D, store="_charts")
|
||||||
|
bubbleChart = MultiSequencePart(expected_type=BubbleChart, store="_charts")
|
||||||
|
|
||||||
|
# axes
|
||||||
|
_axes = MultiSequence()
|
||||||
|
valAx = MultiSequencePart(expected_type=NumericAxis, store="_axes")
|
||||||
|
catAx = MultiSequencePart(expected_type=TextAxis, store="_axes")
|
||||||
|
dateAx = MultiSequencePart(expected_type=DateAxis, store="_axes")
|
||||||
|
serAx = MultiSequencePart(expected_type=SeriesAxis, store="_axes")
|
||||||
|
|
||||||
|
__elements__ = ('layout', '_charts', '_axes', 'dTable', 'spPr')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
layout=None,
|
||||||
|
dTable=None,
|
||||||
|
spPr=None,
|
||||||
|
_charts=(),
|
||||||
|
_axes=(),
|
||||||
|
extLst=None,
|
||||||
|
):
|
||||||
|
self.layout = layout
|
||||||
|
self.dTable = dTable
|
||||||
|
self.spPr = spPr
|
||||||
|
self._charts = _charts
|
||||||
|
self._axes = _axes
|
||||||
|
|
||||||
|
|
||||||
|
def to_tree(self, tagname=None, idx=None, namespace=None):
|
||||||
|
axIds = {ax.axId for ax in self._axes}
|
||||||
|
for chart in self._charts:
|
||||||
|
for id, axis in chart._axes.items():
|
||||||
|
if id not in axIds:
|
||||||
|
setattr(self, axis.tagname, axis)
|
||||||
|
axIds.add(id)
|
||||||
|
|
||||||
|
return super().to_tree(tagname)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_tree(cls, node):
|
||||||
|
self = super().from_tree(node)
|
||||||
|
axes = dict((axis.axId, axis) for axis in self._axes)
|
||||||
|
for chart in self._charts:
|
||||||
|
if isinstance(chart, (ScatterChart, BubbleChart)):
|
||||||
|
x, y = (axes[axId] for axId in chart.axId)
|
||||||
|
chart.x_axis = x
|
||||||
|
chart.y_axis = y
|
||||||
|
continue
|
||||||
|
|
||||||
|
for axId in chart.axId:
|
||||||
|
axis = axes.get(axId)
|
||||||
|
if axis is None and isinstance(chart, _3DBase):
|
||||||
|
# Series Axis can be optional
|
||||||
|
chart.z_axis = None
|
||||||
|
continue
|
||||||
|
if axis.tagname in ("catAx", "dateAx"):
|
||||||
|
chart.x_axis = axis
|
||||||
|
elif axis.tagname == "valAx":
|
||||||
|
chart.y_axis = axis
|
||||||
|
elif axis.tagname == "serAx":
|
||||||
|
chart.z_axis = axis
|
||||||
|
|
||||||
|
return self
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Float,
|
||||||
|
Typed,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
|
||||||
|
from openpyxl.worksheet.page import PrintPageSetup
|
||||||
|
from openpyxl.worksheet.header_footer import HeaderFooter
|
||||||
|
|
||||||
|
|
||||||
|
class PageMargins(Serialisable):
|
||||||
|
"""
|
||||||
|
Identical to openpyxl.worksheet.page.Pagemargins but element names are different :-/
|
||||||
|
"""
|
||||||
|
tagname = "pageMargins"
|
||||||
|
|
||||||
|
l = Float()
|
||||||
|
left = Alias('l')
|
||||||
|
r = Float()
|
||||||
|
right = Alias('r')
|
||||||
|
t = Float()
|
||||||
|
top = Alias('t')
|
||||||
|
b = Float()
|
||||||
|
bottom = Alias('b')
|
||||||
|
header = Float()
|
||||||
|
footer = Float()
|
||||||
|
|
||||||
|
def __init__(self, l=0.75, r=0.75, t=1, b=1, header=0.5, footer=0.5):
|
||||||
|
self.l = l
|
||||||
|
self.r = r
|
||||||
|
self.t = t
|
||||||
|
self.b = b
|
||||||
|
self.header = header
|
||||||
|
self.footer = footer
|
||||||
|
|
||||||
|
|
||||||
|
class PrintSettings(Serialisable):
|
||||||
|
|
||||||
|
tagname = "printSettings"
|
||||||
|
|
||||||
|
headerFooter = Typed(expected_type=HeaderFooter, allow_none=True)
|
||||||
|
pageMargins = Typed(expected_type=PageMargins, allow_none=True)
|
||||||
|
pageSetup = Typed(expected_type=PrintPageSetup, allow_none=True)
|
||||||
|
|
||||||
|
__elements__ = ("headerFooter", "pageMargins", "pageMargins")
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
headerFooter=None,
|
||||||
|
pageMargins=None,
|
||||||
|
pageSetup=None,
|
||||||
|
):
|
||||||
|
self.headerFooter = headerFooter
|
||||||
|
self.pageMargins = pageMargins
|
||||||
|
self.pageSetup = pageSetup
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Copyright (c) 2010-2024 openpyxl
|
||||||
|
|
||||||
|
from openpyxl.descriptors.serialisable import Serialisable
|
||||||
|
from openpyxl.descriptors import (
|
||||||
|
Sequence,
|
||||||
|
Typed,
|
||||||
|
Alias,
|
||||||
|
)
|
||||||
|
from openpyxl.descriptors.excel import ExtensionList
|
||||||
|
from openpyxl.descriptors.nested import (
|
||||||
|
NestedBool,
|
||||||
|
NestedInteger,
|
||||||
|
NestedSet
|
||||||
|
)
|
||||||
|
|
||||||
|
from ._chart import ChartBase
|
||||||
|
from .axis import TextAxis, NumericAxis
|
||||||
|
from .series import Series
|
||||||
|
from .label import DataLabelList
|
||||||
|
|
||||||
|
|
||||||
|
class RadarChart(ChartBase):
|
||||||
|
|
||||||
|
tagname = "radarChart"
|
||||||
|
|
||||||
|
radarStyle = NestedSet(values=(['standard', 'marker', 'filled']))
|
||||||
|
type = Alias("radarStyle")
|
||||||
|
varyColors = NestedBool(nested=True, allow_none=True)
|
||||||
|
ser = Sequence(expected_type=Series, allow_none=True)
|
||||||
|
dLbls = Typed(expected_type=DataLabelList, allow_none=True)
|
||||||
|
dataLabels = Alias("dLbls")
|
||||||
|
extLst = Typed(expected_type=ExtensionList, allow_none=True)
|
||||||
|
|
||||||
|
_series_type = "radar"
|
||||||
|
|
||||||
|
x_axis = Typed(expected_type=TextAxis)
|
||||||
|
y_axis = Typed(expected_type=NumericAxis)
|
||||||
|
|
||||||
|
__elements__ = ('radarStyle', 'varyColors', 'ser', 'dLbls', 'axId')
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
radarStyle="standard",
|
||||||
|
varyColors=None,
|
||||||
|
ser=(),
|
||||||
|
dLbls=None,
|
||||||
|
extLst=None,
|
||||||
|
**kw
|
||||||
|
):
|
||||||
|
self.radarStyle = radarStyle
|
||||||
|
self.varyColors = varyColors
|
||||||
|
self.ser = ser
|
||||||
|
self.dLbls = dLbls
|
||||||
|
self.x_axis = TextAxis()
|
||||||
|
self.y_axis = NumericAxis()
|
||||||
|
super().__init__(**kw)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user