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": {
|
||||
"first_name": "gfgdfggf",
|
||||
"last_name": "gfdgd",
|
||||
"company_name": "gfdgfd",
|
||||
"email": "gfdgd",
|
||||
"phone": "gfdgd",
|
||||
"store_name": "Test7",
|
||||
"country": "gfdgd",
|
||||
"starting_date": "2025-09-17",
|
||||
"first_name": "Denisa",
|
||||
"last_name": "Cirsteas",
|
||||
"company_name": "footprints",
|
||||
"email": "test@test.ro",
|
||||
"phone": "1231231231",
|
||||
"store_name": "TEST",
|
||||
"country": "Romania",
|
||||
"starting_date": "2026-01-01",
|
||||
"duration": 36,
|
||||
"store_types": [
|
||||
"Convenience"
|
||||
"Convenience",
|
||||
"Supermarket"
|
||||
],
|
||||
"open_days_per_month": 30,
|
||||
"convenience_store_type": {
|
||||
"stores_number": 231,
|
||||
"monthly_transactions": 321321321,
|
||||
"stores_number": 4000,
|
||||
"monthly_transactions": 40404040,
|
||||
"has_digital_screens": true,
|
||||
"screen_count": 2,
|
||||
"screen_percentage": 100,
|
||||
@@ -24,13 +25,13 @@
|
||||
"open_days_per_month": 30
|
||||
},
|
||||
"supermarket_store_type": {
|
||||
"stores_number": 0,
|
||||
"monthly_transactions": 0,
|
||||
"has_digital_screens": false,
|
||||
"screen_count": 0,
|
||||
"screen_percentage": 0,
|
||||
"has_in_store_radio": false,
|
||||
"radio_percentage": 0,
|
||||
"stores_number": 200,
|
||||
"monthly_transactions": 20202020,
|
||||
"has_digital_screens": true,
|
||||
"screen_count": 4,
|
||||
"screen_percentage": 100,
|
||||
"has_in_store_radio": true,
|
||||
"radio_percentage": 100,
|
||||
"open_days_per_month": 30
|
||||
},
|
||||
"hypermarket_store_type": {
|
||||
@@ -44,18 +45,18 @@
|
||||
"open_days_per_month": 30
|
||||
},
|
||||
"on_site_channels": [
|
||||
"Mobile App"
|
||||
"Website"
|
||||
],
|
||||
"website_visitors": 0,
|
||||
"app_users": 323213,
|
||||
"website_visitors": 1001001,
|
||||
"app_users": 0,
|
||||
"loyalty_users": 0,
|
||||
"off_site_channels": [
|
||||
"Facebook Business"
|
||||
"Email"
|
||||
],
|
||||
"facebook_followers": 321312,
|
||||
"facebook_followers": 0,
|
||||
"instagram_followers": 0,
|
||||
"google_views": 0,
|
||||
"email_subscribers": 0,
|
||||
"email_subscribers": 100000,
|
||||
"sms_users": 0,
|
||||
"whatsapp_contacts": 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():
|
||||
"""
|
||||
Create a copy of the Excel template, replacing {store_name} with the value from config.json
|
||||
and save it to the output folder.
|
||||
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')
|
||||
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')
|
||||
|
||||
# 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
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const PORT = process.env.PORT || 4444;
|
||||
|
||||
// Middleware
|
||||
app.use(express.static(__dirname)); // Serve static files
|
||||
@@ -78,7 +78,10 @@ app.post('/calculate', async (req, res) => {
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
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}`);
|
||||
|
||||
// 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.
126
update_excel.py
Executable file → Normal file
126
update_excel.py
Executable file → Normal file
@@ -4,12 +4,14 @@ import os
|
||||
import re
|
||||
import openpyxl
|
||||
from openpyxl.utils import get_column_letter
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
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
|
||||
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:
|
||||
excel_path (str): Path to the Excel file to update
|
||||
@@ -186,45 +188,27 @@ def update_excel_variables(excel_path):
|
||||
print(f"Error calculating years for sheet visibility: {e}")
|
||||
calculated_years = [datetime.datetime.now().year]
|
||||
|
||||
# Update sheet names - replace {store_name} with actual store name
|
||||
store_name = user_data.get('store_name', '')
|
||||
if store_name:
|
||||
# Dictionary to store old sheet name to new sheet name mappings
|
||||
sheet_name_mapping = {}
|
||||
|
||||
# Make a copy of the sheet names to avoid modifying during iteration
|
||||
sheet_names = wb.sheetnames.copy()
|
||||
for sheet_name in sheet_names:
|
||||
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:
|
||||
# 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(new_sheet_name.split()[0])
|
||||
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 '{new_sheet_name}' as year {sheet_year} is not in calculated years {calculated_years}")
|
||||
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 '{new_sheet_name}': {e}")
|
||||
print(f"Error extracting year from sheet name '{sheet_name}': {e}")
|
||||
|
||||
# Save the workbook with renamed and hidden sheets
|
||||
# Save the workbook with updated variables and hidden sheets
|
||||
print("Saving workbook with all updates...")
|
||||
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}")
|
||||
return True
|
||||
|
||||
@@ -232,84 +216,6 @@ def update_excel_variables(excel_path):
|
||||
print(f"Error updating Excel file: {e}")
|
||||
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__":
|
||||
# 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