Compare commits
2 Commits
f3978381df
...
bb20000031
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb20000031 | ||
|
|
c91290f1b0 |
1791057
config/alpinebits.log
1791057
config/alpinebits.log
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ dependencies = [
|
|||||||
"generateds>=2.44.3",
|
"generateds>=2.44.3",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"lxml>=6.0.1",
|
"lxml>=6.0.1",
|
||||||
|
"pandas>=2.3.3",
|
||||||
"pushover-complete>=2.0.0",
|
"pushover-complete>=2.0.0",
|
||||||
"pydantic[email]>=2.11.9",
|
"pydantic[email]>=2.11.9",
|
||||||
"pytest>=8.4.2",
|
"pytest>=8.4.2",
|
||||||
|
|||||||
34
sql_analysis.md
Normal file
34
sql_analysis.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
```
|
||||||
|
|
||||||
|
select sum(room.total_revenue::float)
|
||||||
|
|
||||||
|
from alpinebits.conversions as con
|
||||||
|
join alpinebits.room_reservations as room on room.conversion_id = con.id
|
||||||
|
join alpinebits.reservations as res on res.id = con.reservation_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
where con.reservation_id is not null and room.total_revenue is not null
|
||||||
|
and res.start_date <= room.arrival_date + INTERVAL '7 days'
|
||||||
|
;
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
select res.created_at, con.reservation_date, res.start_date, room.arrival_date,res.end_date,
|
||||||
|
room.departure_date, reservation_type, booking_channel, advertising_medium,
|
||||||
|
guest_first_name,guest_last_name, total_revenue,
|
||||||
|
room.room_status
|
||||||
|
|
||||||
|
from alpinebits.conversions as con
|
||||||
|
join alpinebits.room_reservations as room on room.conversion_id = con.id
|
||||||
|
join alpinebits.reservations as res on res.id = con.reservation_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
where con.reservation_id is not null and room.total_revenue is not null
|
||||||
|
and res.start_date <= room.arrival_date + INTERVAL '7 days'
|
||||||
|
order by reservation_date;
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
@@ -42,6 +42,7 @@ from .auth import generate_unique_id, validate_api_key
|
|||||||
from .config_loader import load_config
|
from .config_loader import load_config
|
||||||
from .const import CONF_GOOGLE_ACCOUNT, CONF_HOTEL_ID, CONF_META_ACCOUNT, HttpStatusCode
|
from .const import CONF_GOOGLE_ACCOUNT, CONF_HOTEL_ID, CONF_META_ACCOUNT, HttpStatusCode
|
||||||
from .conversion_service import ConversionService
|
from .conversion_service import ConversionService
|
||||||
|
from .csv_import import CSVImporter
|
||||||
from .customer_service import CustomerService
|
from .customer_service import CustomerService
|
||||||
from .db import Base, ResilientAsyncSession, SessionMaker, create_database_engine
|
from .db import Base, ResilientAsyncSession, SessionMaker, create_database_engine
|
||||||
from .db import Customer as DBCustomer
|
from .db import Customer as DBCustomer
|
||||||
@@ -1154,6 +1155,72 @@ async def handle_wix_form_test(
|
|||||||
raise HTTPException(status_code=500, detail="Error processing test data")
|
raise HTTPException(status_code=500, detail="Error processing test data")
|
||||||
|
|
||||||
|
|
||||||
|
@api_router.post("/admin/import-csv")
|
||||||
|
@limiter.limit(BURST_RATE_LIMIT)
|
||||||
|
async def import_csv_endpoint(
|
||||||
|
request: Request,
|
||||||
|
csv_file_path: str,
|
||||||
|
hotel_code: str | None = None,
|
||||||
|
credentials: tuple = Depends(validate_basic_auth),
|
||||||
|
db_session=Depends(get_async_session),
|
||||||
|
):
|
||||||
|
"""Import reservations from a CSV file (landing_page_form.csv format).
|
||||||
|
|
||||||
|
This endpoint allows importing historical form data into the system.
|
||||||
|
It creates customers and reservations, avoiding duplicates based on:
|
||||||
|
- Name, email, reservation dates
|
||||||
|
- fbclid/gclid tracking IDs
|
||||||
|
|
||||||
|
Requires basic authentication.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
csv_file_path: Path to CSV file (relative to app root)
|
||||||
|
hotel_code: Optional hotel code to override CSV values
|
||||||
|
credentials: Basic auth credentials
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Import statistics including created/skipped counts and any errors
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate file path to prevent path traversal
|
||||||
|
if ".." in csv_file_path or csv_file_path.startswith("/"):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid file path")
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
csv_path = Path(csv_file_path)
|
||||||
|
if not csv_path.exists():
|
||||||
|
# Try relative to app root
|
||||||
|
csv_path = Path() / csv_file_path
|
||||||
|
if not csv_path.exists():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"CSV file not found: {csv_file_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.info(
|
||||||
|
"Starting CSV import from %s (user: %s)", csv_file_path, credentials[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create importer and import
|
||||||
|
importer = CSVImporter(db_session, request.app.state.config)
|
||||||
|
stats = await importer.import_csv_file(str(csv_path), hotel_code, dryrun=False)
|
||||||
|
|
||||||
|
_LOGGER.info("CSV import completed: %s", stats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "CSV import completed",
|
||||||
|
"stats": stats,
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
_LOGGER.error("CSV file not found: %s", e)
|
||||||
|
raise HTTPException(status_code=404, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.exception("Error during CSV import")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error processing CSV: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@api_router.post("/webhook/generic")
|
@api_router.post("/webhook/generic")
|
||||||
@webhook_limiter.limit(WEBHOOK_RATE_LIMIT)
|
@webhook_limiter.limit(WEBHOOK_RATE_LIMIT)
|
||||||
async def handle_generic_webhook(
|
async def handle_generic_webhook(
|
||||||
|
|||||||
567
src/alpine_bits_python/csv_import.py
Normal file
567
src/alpine_bits_python/csv_import.py
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
"""CSV import functionality for landing page forms.
|
||||||
|
|
||||||
|
Handles importing CSV data from landing_page_form.csv and creating/updating
|
||||||
|
reservations and customers in the database.
|
||||||
|
|
||||||
|
Supported CSV columns:
|
||||||
|
- Zeit der Einreichung: Submission timestamp
|
||||||
|
- Angebot auswählen: Room offer
|
||||||
|
- Anreisedatum: Check-in date (YYYY-MM-DD or DD.MM.YYYY)
|
||||||
|
- Abreisedatum: Check-out date (YYYY-MM-DD or DD.MM.YYYY)
|
||||||
|
- Anzahl Erwachsene: Number of adults
|
||||||
|
- Anzahl Kinder: Number of children
|
||||||
|
- Alter Kind 1-10: Ages of children
|
||||||
|
- Anrede: Title/salutation (e.g., "Herr", "Frau")
|
||||||
|
- Vorname: First name (required)
|
||||||
|
- Nachname: Last name (required)
|
||||||
|
- Email: Email address
|
||||||
|
- Phone: Phone number
|
||||||
|
- Message: Customer message/comment
|
||||||
|
- Einwilligung Marketing: Newsletter opt-in (yes/no, checked/unchecked)
|
||||||
|
- utm_Source, utm_Medium, utm_Campaign, utm_Term, utm_Content: UTM tracking
|
||||||
|
- fbclid: Facebook click ID
|
||||||
|
- gclid: Google click ID
|
||||||
|
- hotelid: Hotel ID
|
||||||
|
- hotelname: Hotel name
|
||||||
|
|
||||||
|
Duplicate detection uses: name + email + dates + fbclid/gclid combination
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import date, datetime
|
||||||
|
from io import StringIO
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.exc import MultipleResultsFound
|
||||||
|
|
||||||
|
from .customer_service import CustomerService
|
||||||
|
from .db import Customer, Reservation
|
||||||
|
from .logging_config import get_logger
|
||||||
|
from .reservation_service import ReservationService
|
||||||
|
from .schemas import ReservationData
|
||||||
|
|
||||||
|
_LOGGER = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CSVImporter:
|
||||||
|
"""Handles importing CSV data into the system."""
|
||||||
|
|
||||||
|
# Column rename mapping for CSV import
|
||||||
|
COLUMN_RENAME_MAP = {
|
||||||
|
"Zeit der Einreichung": "submission_timestamp",
|
||||||
|
"Angebot auswählen": "room_offer",
|
||||||
|
"Anreisedatum": "check_in_date",
|
||||||
|
"Abreisedatum": "check_out_date",
|
||||||
|
"Anzahl Erwachsene": "num_adults",
|
||||||
|
"Anzahl Kinder": "num_children",
|
||||||
|
"Alter Kind 1": "child_1_age",
|
||||||
|
"Alter Kind 2": "child_2_age",
|
||||||
|
"Alter Kind 3": "child_3_age",
|
||||||
|
"Alter Kind 4": "child_4_age",
|
||||||
|
"Alter Kind 5": "child_5_age",
|
||||||
|
"Alter Kind 6": "child_6_age",
|
||||||
|
"Alter Kind 7": "child_7_age",
|
||||||
|
"Alter Kind 8": "child_8_age",
|
||||||
|
"Alter Kind 9": "child_9_age",
|
||||||
|
"Alter Kind 10": "child_10_age",
|
||||||
|
"Alter Kind 1.1": "child_1_age_duplicate",
|
||||||
|
"Alter Kind 2.1": "child_2_age_duplicate",
|
||||||
|
"Anrede": "salutation",
|
||||||
|
"Vorname": "first_name",
|
||||||
|
"Nachname": "last_name",
|
||||||
|
"Email": "email",
|
||||||
|
"Phone": "phone",
|
||||||
|
"Message": "message",
|
||||||
|
"Einwilligung Marketing": "newsletter_opt_in",
|
||||||
|
"utm_Source": "utm_source",
|
||||||
|
"utm_Medium": "utm_medium",
|
||||||
|
"utm_Campaign": "utm_campaign",
|
||||||
|
"utm_Term": "utm_term",
|
||||||
|
"utm_Content": "utm_content",
|
||||||
|
"utm_term_id": "utm_term_id",
|
||||||
|
"utm_content_id": "utm_content_id",
|
||||||
|
"gad_source": "gad_source",
|
||||||
|
"gad_campaignid": "gad_campaign_id",
|
||||||
|
"gbraid": "gbraid",
|
||||||
|
"gclid": "gclid",
|
||||||
|
"fbclid": "fbclid",
|
||||||
|
"hotelid": "hotel_id",
|
||||||
|
"hotelname": "hotel_name",
|
||||||
|
"roomtypecode": "room_type_code",
|
||||||
|
"roomclassificationcode": "room_classification_code",
|
||||||
|
"Kinder": "children",
|
||||||
|
# Handle unnamed columns - these get default names like "Unnamed: 0"
|
||||||
|
# The age columns appear to be in positions 6-15 (0-indexed) based on dry run output
|
||||||
|
# We'll handle these via positional renaming in import_csv_file
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, db_session: AsyncSession, config: dict[str, Any]):
|
||||||
|
"""Initialize importer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session: AsyncSession for database operations
|
||||||
|
config: Application configuration dict
|
||||||
|
"""
|
||||||
|
self.db_session = db_session
|
||||||
|
self.config = config
|
||||||
|
self.customer_service = CustomerService(db_session)
|
||||||
|
self.reservation_service = ReservationService(db_session)
|
||||||
|
|
||||||
|
async def find_duplicate_reservation(
|
||||||
|
self,
|
||||||
|
first_name: str,
|
||||||
|
last_name: str,
|
||||||
|
email: Optional[str],
|
||||||
|
start_date: date,
|
||||||
|
end_date: date,
|
||||||
|
fbclid: Optional[str],
|
||||||
|
gclid: Optional[str],
|
||||||
|
) -> Optional[Reservation]:
|
||||||
|
"""Find if a reservation already exists based on unique criteria.
|
||||||
|
|
||||||
|
Uses name, email, dates, fbclid, and gclid to identify duplicates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
first_name: Customer first name
|
||||||
|
last_name: Customer last name
|
||||||
|
email: Customer email
|
||||||
|
start_date: Reservation start date
|
||||||
|
end_date: Reservation end date
|
||||||
|
fbclid: Facebook click ID
|
||||||
|
gclid: Google click ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Existing Reservation if found, None otherwise
|
||||||
|
"""
|
||||||
|
from sqlalchemy import and_, or_, select
|
||||||
|
|
||||||
|
# Build a hash from key fields for quick comparison
|
||||||
|
key_fields = f"{first_name.lower().strip()}|{last_name.lower().strip()}|{email.lower().strip() if email else ''}|{start_date}|{end_date}|{fbclid or ''}|{gclid or ''}"
|
||||||
|
key_hash = hashlib.md5(key_fields.encode()).hexdigest()
|
||||||
|
|
||||||
|
# Query reservations with similar name/email/dates
|
||||||
|
query = (
|
||||||
|
select(Reservation)
|
||||||
|
.select_from(Reservation)
|
||||||
|
.join(Customer, Reservation.customer_id == Customer.id)
|
||||||
|
.where(
|
||||||
|
and_(
|
||||||
|
Reservation.start_date == start_date,
|
||||||
|
Reservation.end_date == end_date,
|
||||||
|
or_(
|
||||||
|
and_(
|
||||||
|
Customer.given_name.ilike(first_name),
|
||||||
|
Customer.surname.ilike(last_name),
|
||||||
|
),
|
||||||
|
(email and Customer.email_address.ilike(email)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await self.db_session.execute(query)
|
||||||
|
candidates = result.scalars().all()
|
||||||
|
|
||||||
|
# Further filter by fbclid/gclid if provided
|
||||||
|
for candidate in candidates:
|
||||||
|
if fbclid and candidate.fbclid == fbclid:
|
||||||
|
return candidate
|
||||||
|
if gclid and candidate.gclid == gclid:
|
||||||
|
return candidate
|
||||||
|
# If no tracking IDs in input, match on name/email/dates
|
||||||
|
if not fbclid and not gclid:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def import_csv_file(
|
||||||
|
self, csv_file_path: str, hotel_code: Optional[str] = None, dryrun: bool = False
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Import reservations from a CSV file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
csv_file_path: Path to CSV file
|
||||||
|
hotel_code: Optional hotel code to override CSV values
|
||||||
|
dryrun: If True, parse and print first 10 rows as JSON without importing
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with import statistics or parsed data (if dryrun=True)
|
||||||
|
"""
|
||||||
|
path = Path(csv_file_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"CSV file not found: {csv_file_path}")
|
||||||
|
|
||||||
|
# Start a transaction - will rollback on any exception
|
||||||
|
await self.db_session.begin()
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Handle dry-run mode
|
||||||
|
if dryrun:
|
||||||
|
df = pd.read_csv(path, encoding="utf-8-sig", nrows=10).fillna("")
|
||||||
|
|
||||||
|
# Rename columns based on mapping
|
||||||
|
rename_dict = {col: self.COLUMN_RENAME_MAP.get(col, col) for col in df.columns}
|
||||||
|
df = df.rename(columns=rename_dict)
|
||||||
|
|
||||||
|
dryrun_data = {
|
||||||
|
"headers": df.columns.tolist(),
|
||||||
|
"rows": df.to_dict(orient="records"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print formatted output
|
||||||
|
print("\n=== CSV Import Dry Run ===")
|
||||||
|
print(f"\nHeaders ({len(df.columns)} columns):")
|
||||||
|
for i, header in enumerate(df.columns, 1):
|
||||||
|
print(f" {i}. {header}")
|
||||||
|
|
||||||
|
print(f"\nFirst {len(df)} rows:")
|
||||||
|
print(df.to_string())
|
||||||
|
|
||||||
|
# Find and print rows with num_children > 0
|
||||||
|
print("\n=== Rows with num_children > 0 ===")
|
||||||
|
for row_num, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
num_children = int(row.get("num_children", 0) or 0)
|
||||||
|
if num_children > 0:
|
||||||
|
print(f"\nRow {row_num + 2}:")
|
||||||
|
print(row.to_string())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return dryrun_data
|
||||||
|
|
||||||
|
# Load CSV with pandas
|
||||||
|
df = pd.read_csv(path, encoding="utf-8-sig").fillna("")
|
||||||
|
|
||||||
|
# Rename columns based on mapping
|
||||||
|
rename_dict = {col: self.COLUMN_RENAME_MAP.get(col, col) for col in df.columns}
|
||||||
|
df = df.rename(columns=rename_dict)
|
||||||
|
|
||||||
|
# Handle positional renaming for child age columns
|
||||||
|
# After "num_children" (column 5, 0-indexed), the next 10 columns are child ages
|
||||||
|
# and columns after that are duplicates (child_1_age_duplicate, child_2_age_duplicate)
|
||||||
|
col_list = list(df.columns)
|
||||||
|
if "num_children" in col_list:
|
||||||
|
num_children_idx = col_list.index("num_children")
|
||||||
|
# The 10 columns after num_children are child ages (1-10)
|
||||||
|
for i in range(1, 11):
|
||||||
|
if num_children_idx + i < len(col_list):
|
||||||
|
col_name = col_list[num_children_idx + i]
|
||||||
|
# Only rename if not already renamed
|
||||||
|
if not col_name.startswith("child_"):
|
||||||
|
df.rename(columns={col_name: f"child_{i}_age"}, inplace=True)
|
||||||
|
col_list[num_children_idx + i] = f"child_{i}_age"
|
||||||
|
|
||||||
|
# Debug: log the column names after renaming
|
||||||
|
_LOGGER.debug("CSV columns after rename: %s", list(df.columns))
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"total_rows": 0,
|
||||||
|
"skipped_empty": 0,
|
||||||
|
"created_customers": 0,
|
||||||
|
"existing_customers": 0,
|
||||||
|
"created_reservations": 0,
|
||||||
|
"skipped_duplicates": 0,
|
||||||
|
"errors": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to parse dates
|
||||||
|
def parse_date_str(date_str: str) -> Optional[date]:
|
||||||
|
"""Parse date string in various formats."""
|
||||||
|
if not date_str or not isinstance(date_str, str):
|
||||||
|
return None
|
||||||
|
date_str = date_str.strip()
|
||||||
|
for fmt in ["%Y-%m-%d", "%d.%m.%Y", "%d/%m/%Y"]:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(date_str, fmt).date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Process each row - stop on first error for debugging
|
||||||
|
for row_num, row in df.iterrows():
|
||||||
|
stats["total_rows"] += 1
|
||||||
|
row_num += 2 # Convert to 1-based and account for header
|
||||||
|
|
||||||
|
# Extract required fields (using renamed column names)
|
||||||
|
first_name = str(row.get("first_name", "")).strip()
|
||||||
|
last_name = str(row.get("last_name", "")).strip()
|
||||||
|
email = str(row.get("email", "")).strip()
|
||||||
|
|
||||||
|
# Validate required name fields
|
||||||
|
if not first_name or not last_name:
|
||||||
|
_LOGGER.warning("Skipping row %d: missing name", row_num)
|
||||||
|
stats["skipped_empty"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse and validate dates
|
||||||
|
start_date_str = str(row.get("check_in_date", "")).strip()
|
||||||
|
end_date_str = str(row.get("check_out_date", "")).strip()
|
||||||
|
|
||||||
|
start_date = parse_date_str(start_date_str)
|
||||||
|
end_date = parse_date_str(end_date_str)
|
||||||
|
|
||||||
|
if not start_date or not end_date:
|
||||||
|
_LOGGER.warning("Skipping row %d: invalid or missing dates", row_num)
|
||||||
|
stats["skipped_empty"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get tracking IDs for duplicate detection
|
||||||
|
fbclid = str(row.get("fbclid", "")).strip() or None
|
||||||
|
gclid = str(row.get("gclid", "")).strip() or None
|
||||||
|
|
||||||
|
# Check for duplicate reservation
|
||||||
|
existing_res = await self.find_duplicate_reservation(
|
||||||
|
first_name, last_name, email or None, start_date, end_date, fbclid, gclid
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_res:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Skipping row %d: duplicate reservation found (ID: %s)",
|
||||||
|
row_num,
|
||||||
|
existing_res.unique_id,
|
||||||
|
)
|
||||||
|
stats["skipped_duplicates"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build customer data from CSV row
|
||||||
|
customer_data = {
|
||||||
|
"given_name": first_name,
|
||||||
|
"surname": last_name,
|
||||||
|
"name_prefix": str(row.get("salutation", "")).strip() or None,
|
||||||
|
"email_address": email or None,
|
||||||
|
"phone": str(row.get("phone", "")).strip() or None,
|
||||||
|
"email_newsletter": self._parse_bool(row.get("newsletter_opt_in")),
|
||||||
|
"address_line": None,
|
||||||
|
"city_name": None,
|
||||||
|
"postal_code": None,
|
||||||
|
"country_code": None,
|
||||||
|
"gender": None,
|
||||||
|
"birth_date": None,
|
||||||
|
"language": "de",
|
||||||
|
"address_catalog": False,
|
||||||
|
"name_title": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get or create customer
|
||||||
|
customer = await self._find_or_create_customer(customer_data)
|
||||||
|
if customer.id is None:
|
||||||
|
await self.db_session.refresh(customer)
|
||||||
|
stats["created_customers"] += 1
|
||||||
|
else:
|
||||||
|
stats["existing_customers"] += 1
|
||||||
|
|
||||||
|
# Build reservation data from CSV row
|
||||||
|
num_adults = int(row.get("num_adults", 1) or 1)
|
||||||
|
num_children = int(row.get("num_children", 0) or 0)
|
||||||
|
|
||||||
|
# Extract children ages from columns (including duplicates)
|
||||||
|
children_ages = []
|
||||||
|
|
||||||
|
# Try to extract ages from renamed columns first
|
||||||
|
# Check primary child age columns (1-10)
|
||||||
|
for i in range(1, 11):
|
||||||
|
age_key = f"child_{i}_age"
|
||||||
|
age_val = row.get(age_key, "")
|
||||||
|
if age_val != "" and age_val is not None:
|
||||||
|
try:
|
||||||
|
# Handle both int and float values (e.g., 3, 3.0)
|
||||||
|
age = int(float(age_val))
|
||||||
|
if 0 <= age <= 17:
|
||||||
|
children_ages.append(age)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check for duplicate child age columns (e.g., child_1_age_duplicate, child_2_age_duplicate)
|
||||||
|
for i in range(1, 3): # Only 1.1 and 2.1 duplicates mentioned
|
||||||
|
age_key = f"child_{i}_age_duplicate"
|
||||||
|
age_val = row.get(age_key, "")
|
||||||
|
if age_val != "" and age_val is not None:
|
||||||
|
try:
|
||||||
|
# Handle both int and float values (e.g., 3, 3.0)
|
||||||
|
age = int(float(age_val))
|
||||||
|
if 0 <= age <= 17:
|
||||||
|
children_ages.append(age)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Debug: log extraction details
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Row %d: num_children=%d, extracted %d ages: %s",
|
||||||
|
row_num,
|
||||||
|
num_children,
|
||||||
|
len(children_ages),
|
||||||
|
children_ages,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we extracted ages but num_children says there are different number,
|
||||||
|
# compact the list to match num_children. Remove ages "0" first
|
||||||
|
if len(children_ages) > num_children:
|
||||||
|
# Remove ages "0" first, but only as many as needed
|
||||||
|
num_to_remove = len(children_ages) - num_children
|
||||||
|
|
||||||
|
for _ in range(num_to_remove):
|
||||||
|
if 0 in children_ages:
|
||||||
|
children_ages.remove(0)
|
||||||
|
else:
|
||||||
|
# If no "0" ages left, just remove the last one
|
||||||
|
children_ages.pop()
|
||||||
|
|
||||||
|
|
||||||
|
# Generate unique ID (use submission timestamp if available, else row number)
|
||||||
|
submission_ts = str(row.get("submission_timestamp", "")).strip()
|
||||||
|
if submission_ts:
|
||||||
|
submission_id = submission_ts
|
||||||
|
else:
|
||||||
|
submission_id = f"csv_import_{row_num}_{datetime.now().isoformat()}"
|
||||||
|
|
||||||
|
# Determine hotel code and name
|
||||||
|
final_hotel_code = (
|
||||||
|
hotel_code
|
||||||
|
or str(row.get("hotel_id", "")).strip()
|
||||||
|
or self.config.get("default_hotel_code", "123")
|
||||||
|
)
|
||||||
|
final_hotel_name = (
|
||||||
|
str(row.get("hotel_name", "")).strip()
|
||||||
|
or self.config.get("default_hotel_name", "Frangart Inn")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse room type fields if available
|
||||||
|
room_type_code = str(row.get("room_type_code", "")).strip() or None
|
||||||
|
room_class_code = str(row.get("room_classification_code", "")).strip() or None
|
||||||
|
|
||||||
|
# Build and validate ReservationData
|
||||||
|
reservation = ReservationData(
|
||||||
|
unique_id=submission_id,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
num_adults=num_adults,
|
||||||
|
num_children=num_children,
|
||||||
|
children_ages=children_ages,
|
||||||
|
hotel_code=final_hotel_code,
|
||||||
|
hotel_name=final_hotel_name,
|
||||||
|
offer=str(row.get("room_offer", "")).strip() or None,
|
||||||
|
user_comment=str(row.get("message", "")).strip() or None,
|
||||||
|
fbclid=fbclid,
|
||||||
|
gclid=gclid,
|
||||||
|
utm_source=str(row.get("utm_source", "")).strip() or None,
|
||||||
|
utm_medium=str(row.get("utm_medium", "")).strip() or None,
|
||||||
|
utm_campaign=str(row.get("utm_campaign", "")).strip() or None,
|
||||||
|
utm_term=str(row.get("utm_term", "")).strip() or None,
|
||||||
|
utm_content=str(row.get("utm_content", "")).strip() or None,
|
||||||
|
room_type_code=room_type_code,
|
||||||
|
room_classification_code=room_class_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create reservation if customer exists
|
||||||
|
if customer.id:
|
||||||
|
await self.reservation_service.create_reservation(
|
||||||
|
reservation, customer.id
|
||||||
|
)
|
||||||
|
stats["created_reservations"] += 1
|
||||||
|
_LOGGER.info("Created reservation for %s %s", first_name, last_name)
|
||||||
|
else:
|
||||||
|
raise ValueError("Failed to get or create customer")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Rollback transaction on any error
|
||||||
|
await self.db_session.rollback()
|
||||||
|
_LOGGER.exception("CSV import failed, rolling back all changes")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Commit transaction on success
|
||||||
|
await self.db_session.commit()
|
||||||
|
_LOGGER.info("CSV import completed successfully. Stats: %s", stats)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def _parse_bool(self, value: Any) -> Optional[bool]:
|
||||||
|
"""Parse various boolean representations to bool or None.
|
||||||
|
|
||||||
|
Handles: 'yes', 'no', 'true', 'false', 'checked', 'unchecked', etc.
|
||||||
|
Returns None if value is empty or invalid.
|
||||||
|
"""
|
||||||
|
if not value or (isinstance(value, str) and not value.strip()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
str_val = str(value).lower().strip()
|
||||||
|
if str_val in ("yes", "true", "checked", "1", "y", "t"):
|
||||||
|
return True
|
||||||
|
elif str_val in ("no", "false", "unchecked", "0", "n", "f"):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _find_or_create_customer(self, customer_data: dict) -> Customer:
|
||||||
|
"""Find existing customer or create new one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer_data: Customer data dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Customer instance
|
||||||
|
"""
|
||||||
|
from sqlalchemy import and_, select
|
||||||
|
|
||||||
|
# Try to find by email and name
|
||||||
|
email = customer_data.get("email_address")
|
||||||
|
given_name = customer_data.get("given_name")
|
||||||
|
surname = customer_data.get("surname")
|
||||||
|
|
||||||
|
if email or (given_name and surname):
|
||||||
|
query = select(Customer)
|
||||||
|
filters = []
|
||||||
|
|
||||||
|
if email:
|
||||||
|
filters.append(Customer.email_address == email)
|
||||||
|
if given_name and surname:
|
||||||
|
filters.append(
|
||||||
|
and_(
|
||||||
|
Customer.given_name.ilike(given_name),
|
||||||
|
Customer.surname.ilike(surname),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
query = query.where(or_(*filters))
|
||||||
|
result = await self.db_session.execute(query)
|
||||||
|
try:
|
||||||
|
existing = result.scalar()
|
||||||
|
except MultipleResultsFound:
|
||||||
|
compiled_query = query.compile(compile_kwargs={"literal_binds": True})
|
||||||
|
_LOGGER.error(compiled_query)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# Update customer data if needed
|
||||||
|
try:
|
||||||
|
existing_customer = await self.customer_service.update_customer(
|
||||||
|
existing, customer_data
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
print(customer_data)
|
||||||
|
print("---")
|
||||||
|
print(existing)
|
||||||
|
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
return existing_customer
|
||||||
|
|
||||||
|
# Create new customer
|
||||||
|
return await self.customer_service.create_customer(customer_data)
|
||||||
@@ -253,6 +253,9 @@ class Customer(Base):
|
|||||||
name_title = Column(String) # Added for XML
|
name_title = Column(String) # Added for XML
|
||||||
reservations = relationship("Reservation", back_populates="customer")
|
reservations = relationship("Reservation", back_populates="customer")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Customer (id={self.id}, contact_id={self.contact_id}, email={self.email_address}), given_name={self.given_name} surname={self.surname}), phone={self.phone}, city={self.city_name}), postal_code={self.postal_code}, country_code={self.country_code})"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_and_hash(value):
|
def _normalize_and_hash(value):
|
||||||
"""Normalize and hash a value according to Meta Conversion API requirements."""
|
"""Normalize and hash a value according to Meta Conversion API requirements."""
|
||||||
|
|||||||
128
uv.lock
generated
128
uv.lock
generated
@@ -28,6 +28,7 @@ dependencies = [
|
|||||||
{ name = "generateds" },
|
{ name = "generateds" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "lxml" },
|
{ name = "lxml" },
|
||||||
|
{ name = "pandas" },
|
||||||
{ name = "pushover-complete" },
|
{ name = "pushover-complete" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -58,6 +59,7 @@ requires-dist = [
|
|||||||
{ name = "generateds", specifier = ">=2.44.3" },
|
{ name = "generateds", specifier = ">=2.44.3" },
|
||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "lxml", specifier = ">=6.0.1" },
|
{ name = "lxml", specifier = ">=6.0.1" },
|
||||||
|
{ name = "pandas", specifier = ">=2.3.3" },
|
||||||
{ name = "pushover-complete", specifier = ">=2.0.0" },
|
{ name = "pushover-complete", specifier = ">=2.0.0" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.9" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.9" },
|
||||||
{ name = "pytest", specifier = ">=8.4.2" },
|
{ name = "pytest", specifier = ">=8.4.2" },
|
||||||
@@ -444,6 +446,8 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||||
@@ -451,6 +455,8 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -607,6 +613,58 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "25.0"
|
version = "25.0"
|
||||||
@@ -616,6 +674,46 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "2.3.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "tzdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -777,6 +875,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -786,6 +896,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
@@ -991,6 +1110,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untokenize"
|
name = "untokenize"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user