Compare commits
40 Commits
v1.1.3
...
a343013eed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a343013eed | ||
|
|
7380fa4378 | ||
|
|
642b6cb7a5 | ||
|
|
ebcf2c22dd | ||
|
|
373cf0882a | ||
|
|
6c2ce2dc08 | ||
|
|
7f25fb2b02 | ||
|
|
54c002ac96 | ||
|
|
382bf2334a | ||
|
|
06739ebea9 | ||
|
|
384fb2b558 | ||
|
|
8d4ccc4041 | ||
|
|
6688a9a465 | ||
|
|
12f245ae06 | ||
| 679785dd1c | |||
| 9eb993cba5 | |||
|
|
b79288f6b6 | ||
|
|
52114a7443 | ||
|
|
784ff0e5da | ||
| 0a6c4f64e8 | |||
|
|
958e48b40a | ||
|
|
76176f8a79 | ||
|
|
4416397a69 | ||
|
|
ff00edf35d | ||
|
|
7b539ea42f | ||
|
|
5fb313d4cc | ||
|
|
553fcc7a24 | ||
|
|
0f7f1532a0 | ||
|
|
4cfc00abb1 | ||
|
|
9f80f49693 | ||
|
|
e95b680ff0 | ||
|
|
4ceb50f9ed | ||
|
|
eda3bf505e | ||
|
|
d9f814ca64 | ||
|
|
06ea9caded | ||
|
|
2af9558b35 | ||
|
|
e9cf606dde | ||
|
|
7ed6ceecc5 | ||
|
|
31b6c7eceb | ||
|
|
85214344ef |
10
.github/workflows/publish.yaml
vendored
10
.github/workflows/publish.yaml
vendored
@@ -13,15 +13,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- name: Install Python 3.13
|
||||
run: uv python install 3.13
|
||||
- name: Build
|
||||
run: uv build
|
||||
run: uv auth login gitea.linter-home.com --username jonas --password ${{ secrets.CI_TOKEN }} && uv build
|
||||
# Check that basic features work and we didn't miss to include crucial files
|
||||
- name: Smoke test (wheel)
|
||||
run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py
|
||||
- name: Smoke test (source distribution)
|
||||
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py
|
||||
- name: Publish
|
||||
run: uv publish --publish-url https://gitea.linter-home.com.com/api/packages/jonas/pypi --username jonas --password ${{ secrets.GITEA_TOKEN }}
|
||||
run: uv publish --publish-url https://gitea.linter-home.com/api/packages/jonas/pypi --username jonas --password ${{ secrets.CI_TOKEN }}
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -13,3 +13,13 @@ wheels/
|
||||
|
||||
# exclude ruff cache
|
||||
.ruff_cache/
|
||||
|
||||
# ignore test_data content but keep the folder
|
||||
test_data/*
|
||||
|
||||
|
||||
# ignore secrets
|
||||
secrets.yaml
|
||||
|
||||
# ignore db
|
||||
alpinebits.db
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -3,5 +3,6 @@
|
||||
"test"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
class GivenNameType(GeneratedsSuper):
|
||||
__hash__ = GeneratedsSuper.__hash__
|
||||
subclass = None
|
||||
superclass = None
|
||||
def __init__(self, valueOf_=None, gds_collector_=None, **kwargs_):
|
||||
self.gds_collector_ = gds_collector_
|
||||
self.gds_elementtree_node_ = None
|
||||
self.original_tagname_ = None
|
||||
self.parent_object_ = kwargs_.get('parent_object_')
|
||||
self.ns_prefix_ = None
|
||||
self.valueOf_ = valueOf_
|
||||
def factory(*args_, **kwargs_):
|
||||
if CurrentSubclassModule_ is not None:
|
||||
subclass = getSubclassFromModule_(
|
||||
CurrentSubclassModule_, GivenNameType)
|
||||
if subclass is not None:
|
||||
return subclass(*args_, **kwargs_)
|
||||
if GivenNameType.subclass:
|
||||
return GivenNameType.subclass(*args_, **kwargs_)
|
||||
else:
|
||||
return GivenNameType(*args_, **kwargs_)
|
||||
factory = staticmethod(factory)
|
||||
def get_ns_prefix_(self):
|
||||
return self.ns_prefix_
|
||||
def set_ns_prefix_(self, ns_prefix):
|
||||
self.ns_prefix_ = ns_prefix
|
||||
def get_valueOf_(self): return self.valueOf_
|
||||
def set_valueOf_(self, valueOf_): self.valueOf_ = valueOf_
|
||||
def validate_StringLength1to64(self, value):
|
||||
result = True
|
||||
# Validate type StringLength1to64, a restriction on xs:string.
|
||||
if value is not None and Validate_simpletypes_ and self.gds_collector_ is not None:
|
||||
if not isinstance(value, str):
|
||||
lineno = self.gds_get_node_lineno_()
|
||||
self.gds_collector_.add_message('Value "%(value)s"%(lineno)s is not of the correct base simple type (str)' % {"value": value, "lineno": lineno, })
|
||||
return False
|
||||
if len(value) > 64:
|
||||
lineno = self.gds_get_node_lineno_()
|
||||
self.gds_collector_.add_message('Value "%(value)s"%(lineno)s does not match xsd maxLength restriction on StringLength1to64' % {"value" : encode_str_2_3(value), "lineno": lineno} )
|
||||
result = False
|
||||
if len(value) < 1:
|
||||
lineno = self.gds_get_node_lineno_()
|
||||
self.gds_collector_.add_message('Value "%(value)s"%(lineno)s does not match xsd minLength restriction on StringLength1to64' % {"value" : encode_str_2_3(value), "lineno": lineno} )
|
||||
result = False
|
||||
return result
|
||||
def has__content(self):
|
||||
if (
|
||||
(1 if type(self.valueOf_) in [int,float] else self.valueOf_)
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
def export(self, outfile, level, namespaceprefix_='', namespacedef_='', name_='GivenNameType', pretty_print=True):
|
||||
imported_ns_def_ = GenerateDSNamespaceDefs_.get('GivenNameType')
|
||||
if imported_ns_def_ is not None:
|
||||
namespacedef_ = imported_ns_def_
|
||||
if pretty_print:
|
||||
eol_ = '\n'
|
||||
else:
|
||||
eol_ = ''
|
||||
if self.original_tagname_ is not None and name_ == 'GivenNameType':
|
||||
name_ = self.original_tagname_
|
||||
if UseCapturedNS_ and self.ns_prefix_:
|
||||
namespaceprefix_ = self.ns_prefix_ + ':'
|
||||
showIndent(outfile, level, pretty_print)
|
||||
outfile.write('<%s%s%s' % (namespaceprefix_, name_, namespacedef_ and ' ' + namespacedef_ or '', ))
|
||||
already_processed = set()
|
||||
self._exportAttributes(outfile, level, already_processed, namespaceprefix_, name_='GivenNameType')
|
||||
outfile.write('>')
|
||||
self._exportChildren(outfile, level + 1, namespaceprefix_, namespacedef_, name_, pretty_print=pretty_print)
|
||||
outfile.write(self.convert_unicode(self.valueOf_))
|
||||
outfile.write('</%s%s>%s' % (namespaceprefix_, name_, eol_))
|
||||
def _exportAttributes(self, outfile, level, already_processed, namespaceprefix_='', name_='GivenNameType'):
|
||||
pass
|
||||
def _exportChildren(self, outfile, level, namespaceprefix_='', namespacedef_='', name_='GivenNameType', fromsubclass_=False, pretty_print=True):
|
||||
pass
|
||||
def build(self, node, gds_collector_=None):
|
||||
self.gds_collector_ = gds_collector_
|
||||
if SaveElementTreeNode:
|
||||
self.gds_elementtree_node_ = node
|
||||
already_processed = set()
|
||||
self.ns_prefix_ = node.prefix
|
||||
self._buildAttributes(node, node.attrib, already_processed)
|
||||
self.valueOf_ = get_all_text_(node)
|
||||
for child in node:
|
||||
nodeName_ = Tag_pattern_.match(child.tag).groups()[-1]
|
||||
self._buildChildren(child, node, nodeName_, gds_collector_=gds_collector_)
|
||||
return self
|
||||
def _buildAttributes(self, node, attrs, already_processed):
|
||||
pass
|
||||
def _buildChildren(self, child_, node, nodeName_, fromsubclass_=False, gds_collector_=None):
|
||||
pass
|
||||
# end class GivenNameType
|
||||
@@ -1,93 +0,0 @@
|
||||
class SurnameType(GeneratedsSuper):
|
||||
__hash__ = GeneratedsSuper.__hash__
|
||||
subclass = None
|
||||
superclass = None
|
||||
def __init__(self, valueOf_=None, gds_collector_=None, **kwargs_):
|
||||
self.gds_collector_ = gds_collector_
|
||||
self.gds_elementtree_node_ = None
|
||||
self.original_tagname_ = None
|
||||
self.parent_object_ = kwargs_.get('parent_object_')
|
||||
self.ns_prefix_ = None
|
||||
self.valueOf_ = valueOf_
|
||||
def factory(*args_, **kwargs_):
|
||||
if CurrentSubclassModule_ is not None:
|
||||
subclass = getSubclassFromModule_(
|
||||
CurrentSubclassModule_, SurnameType)
|
||||
if subclass is not None:
|
||||
return subclass(*args_, **kwargs_)
|
||||
if SurnameType.subclass:
|
||||
return SurnameType.subclass(*args_, **kwargs_)
|
||||
else:
|
||||
return SurnameType(*args_, **kwargs_)
|
||||
factory = staticmethod(factory)
|
||||
def get_ns_prefix_(self):
|
||||
return self.ns_prefix_
|
||||
def set_ns_prefix_(self, ns_prefix):
|
||||
self.ns_prefix_ = ns_prefix
|
||||
def get_valueOf_(self): return self.valueOf_
|
||||
def set_valueOf_(self, valueOf_): self.valueOf_ = valueOf_
|
||||
def validate_StringLength1to64(self, value):
|
||||
result = True
|
||||
# Validate type StringLength1to64, a restriction on xs:string.
|
||||
if value is not None and Validate_simpletypes_ and self.gds_collector_ is not None:
|
||||
if not isinstance(value, str):
|
||||
lineno = self.gds_get_node_lineno_()
|
||||
self.gds_collector_.add_message('Value "%(value)s"%(lineno)s is not of the correct base simple type (str)' % {"value": value, "lineno": lineno, })
|
||||
return False
|
||||
if len(value) > 64:
|
||||
lineno = self.gds_get_node_lineno_()
|
||||
self.gds_collector_.add_message('Value "%(value)s"%(lineno)s does not match xsd maxLength restriction on StringLength1to64' % {"value" : encode_str_2_3(value), "lineno": lineno} )
|
||||
result = False
|
||||
if len(value) < 1:
|
||||
lineno = self.gds_get_node_lineno_()
|
||||
self.gds_collector_.add_message('Value "%(value)s"%(lineno)s does not match xsd minLength restriction on StringLength1to64' % {"value" : encode_str_2_3(value), "lineno": lineno} )
|
||||
result = False
|
||||
return result
|
||||
def has__content(self):
|
||||
if (
|
||||
(1 if type(self.valueOf_) in [int,float] else self.valueOf_)
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
def export(self, outfile, level, namespaceprefix_='', namespacedef_='', name_='SurnameType', pretty_print=True):
|
||||
imported_ns_def_ = GenerateDSNamespaceDefs_.get('SurnameType')
|
||||
if imported_ns_def_ is not None:
|
||||
namespacedef_ = imported_ns_def_
|
||||
if pretty_print:
|
||||
eol_ = '\n'
|
||||
else:
|
||||
eol_ = ''
|
||||
if self.original_tagname_ is not None and name_ == 'SurnameType':
|
||||
name_ = self.original_tagname_
|
||||
if UseCapturedNS_ and self.ns_prefix_:
|
||||
namespaceprefix_ = self.ns_prefix_ + ':'
|
||||
showIndent(outfile, level, pretty_print)
|
||||
outfile.write('<%s%s%s' % (namespaceprefix_, name_, namespacedef_ and ' ' + namespacedef_ or '', ))
|
||||
already_processed = set()
|
||||
self._exportAttributes(outfile, level, already_processed, namespaceprefix_, name_='SurnameType')
|
||||
outfile.write('>')
|
||||
self._exportChildren(outfile, level + 1, namespaceprefix_, namespacedef_, name_, pretty_print=pretty_print)
|
||||
outfile.write(self.convert_unicode(self.valueOf_))
|
||||
outfile.write('</%s%s>%s' % (namespaceprefix_, name_, eol_))
|
||||
def _exportAttributes(self, outfile, level, already_processed, namespaceprefix_='', name_='SurnameType'):
|
||||
pass
|
||||
def _exportChildren(self, outfile, level, namespaceprefix_='', namespacedef_='', name_='SurnameType', fromsubclass_=False, pretty_print=True):
|
||||
pass
|
||||
def build(self, node, gds_collector_=None):
|
||||
self.gds_collector_ = gds_collector_
|
||||
if SaveElementTreeNode:
|
||||
self.gds_elementtree_node_ = node
|
||||
already_processed = set()
|
||||
self.ns_prefix_ = node.prefix
|
||||
self._buildAttributes(node, node.attrib, already_processed)
|
||||
self.valueOf_ = get_all_text_(node)
|
||||
for child in node:
|
||||
nodeName_ = Tag_pattern_.match(child.tag).groups()[-1]
|
||||
self._buildChildren(child, node, nodeName_, gds_collector_=gds_collector_)
|
||||
return self
|
||||
def _buildAttributes(self, node, attrs, already_processed):
|
||||
pass
|
||||
def _buildChildren(self, child_, node, nodeName_, fromsubclass_=False, gds_collector_=None):
|
||||
pass
|
||||
# end class SurnameType
|
||||
66
Dockerfile
Normal file
66
Dockerfile
Normal file
@@ -0,0 +1,66 @@
|
||||
# Multi-stage build for smaller final image
|
||||
FROM python:3.13-slim AS builder
|
||||
|
||||
# Install uv for fast dependency management
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy dependency files
|
||||
COPY pyproject.toml uv.lock README.md ./
|
||||
|
||||
ARG CI_TOKEN
|
||||
|
||||
# Install dependencies in a virtual environment
|
||||
RUN uv sync --frozen --no-cache
|
||||
|
||||
# Production stage
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Create non-root user for security
|
||||
RUN groupadd -r appuser && useradd -r -g appuser -u 1000 appuser
|
||||
|
||||
# Install uv in production image
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy virtual environment from builder stage
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
|
||||
# Copy application code
|
||||
COPY src/ ./src/
|
||||
|
||||
# Create directories and set permissions
|
||||
RUN mkdir -p /app/logs && \
|
||||
chown -R appuser:appuser /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Set environment variables
|
||||
ENV PATH="/app/.venv/bin:$PATH" \
|
||||
PYTHONPATH="/app/src" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Expose port (non-privileged port)
|
||||
EXPOSE 8000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=120s --timeout=10s --start-period=60s --retries=3 \
|
||||
CMD python -c "import requests; requests.get('http://localhost:8000/api/health', timeout=5)"
|
||||
|
||||
# Run the application with uvicorn
|
||||
WORKDIR /app/src
|
||||
CMD uvicorn alpine_bits_python.api:app \
|
||||
--host 0.0.0.0 \
|
||||
--port 8000 \
|
||||
--workers 4 \
|
||||
--log-level info \
|
||||
--access-log \
|
||||
--forwarded-allow-ips "${FORWARDED_ALLOW_IPS:-127.0.0.1}" \
|
||||
--proxy-headers \
|
||||
--no-server-header
|
||||
18
README.md
18
README.md
@@ -8,3 +8,21 @@ When ASA wants to know our GuestRequests from the Landing page then they are the
|
||||
|
||||
Just for GuestRequests this should be fine however.
|
||||
|
||||
|
||||
|
||||
# Wix formular parsing Notizen
|
||||
|
||||
|
||||
### Kontaktinformationen
|
||||
|
||||
|
||||
|
||||
#### Werbeparameter
|
||||
|
||||
|
||||
|
||||
|
||||
field:angebot_auswaehlen -> Kommentar mit Angebot
|
||||
|
||||
|
||||
|
||||
|
||||
16
config/config.yaml
Normal file
16
config/config.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
# AlpineBits Python config
|
||||
# Use annotatedyaml for secrets and environment-specific overrides
|
||||
|
||||
database:
|
||||
url: "sqlite+aiosqlite:///alpinebits.db" # For local dev, use SQLite. For prod, override with PostgreSQL URL.
|
||||
# url: "postgresql://user:password@host:port/dbname" # Example for Postgres
|
||||
|
||||
alpine_bits_auth:
|
||||
- hotel_id: "123"
|
||||
hotel_name: "Frangart Inn"
|
||||
username: "alice"
|
||||
password: !secret ALICE_PASSWORD
|
||||
- hotel_id: "456"
|
||||
hotel_name: "Bemelmans"
|
||||
username: "bob"
|
||||
password: !secret BOB_PASSWORD
|
||||
250
logs/wix_test_data_20250929_154411.json
Normal file
250
logs/wix_test_data_20250929_154411.json
Normal file
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"timestamp": "2025-09-29T15:44:11.839852",
|
||||
"client_ip": "127.0.0.1",
|
||||
"headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "6920"
|
||||
},
|
||||
"data": {
|
||||
"data": {
|
||||
"formName": "Contact us",
|
||||
"submissions": [
|
||||
{
|
||||
"label": "Angebot auswählen",
|
||||
"value": "Herbstferien - Familienzeit mit Dolomitenblick"
|
||||
},
|
||||
{
|
||||
"label": "Anreisedatum",
|
||||
"value": "2025-10-31"
|
||||
},
|
||||
{
|
||||
"label": "Abreisedatum",
|
||||
"value": "2025-11-02"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Erwachsene",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Kinder",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"label": "Alter Kind 1",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"label": "Alter Kind 2",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"label": "Alter Kind 3",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"label": "Anrede",
|
||||
"value": "Frau"
|
||||
},
|
||||
{
|
||||
"label": "Vorname",
|
||||
"value": "Elena"
|
||||
},
|
||||
{
|
||||
"label": "Nachname",
|
||||
"value": "Battiloro"
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "e.battiloro1@gmail.com"
|
||||
},
|
||||
{
|
||||
"label": "Phone",
|
||||
"value": "+39 333 767 3262"
|
||||
},
|
||||
{
|
||||
"label": "Einwilligung Marketing",
|
||||
"value": "Non selezionato"
|
||||
},
|
||||
{
|
||||
"label": "utm_Source",
|
||||
"value": "ig"
|
||||
},
|
||||
{
|
||||
"label": "utm_Medium",
|
||||
"value": "Instagram_Stories"
|
||||
},
|
||||
{
|
||||
"label": "utm_Campaign",
|
||||
"value": "Conversions_Hotel_Bemelmans_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_Term",
|
||||
"value": "Cold_Traffic_Conversions_Hotel_Bemelmans_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_Content",
|
||||
"value": "Grafik_4_Spätsommer_23.08-07.09_Landingpage_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_term_id",
|
||||
"value": "120232007764490196"
|
||||
},
|
||||
{
|
||||
"label": "utm_content_id",
|
||||
"value": "120232007764490196"
|
||||
},
|
||||
{
|
||||
"label": "gad_source",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gad_campaignid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gbraid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gclid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "fbclid",
|
||||
"value": "PAZXh0bgNhZW0BMABhZGlkAasmYBhk4DQBp02L46Rl1jAuccxsOaeFSv7WSFnP-MQCsOrz9yDnKRH4hwZ7GEgxF9gy0_OF_aem_qSvrs6xsBkvTaI_Y9_hfnQ"
|
||||
}
|
||||
],
|
||||
"field:date_picker_7e65": "2025-11-02",
|
||||
"field:number_7cf5": "2",
|
||||
"field:utm_source": "ig",
|
||||
"submissionTime": "2025-09-28T13:26:07.938Z",
|
||||
"field:alter_kind_3": "3",
|
||||
"field:gad_source": "",
|
||||
"field:form_field_5a7b": "Non selezionato",
|
||||
"field:gad_campaignid": "",
|
||||
"field:utm_medium": "Instagram_Stories",
|
||||
"field:utm_term_id": "120232007764490196",
|
||||
"context": {
|
||||
"metaSiteId": "1dea821c-8168-4736-96e4-4b92e8b364cf",
|
||||
"activationId": "3fd865e1-f44a-49d2-ae29-19cf77ee488a"
|
||||
},
|
||||
"field:email_5139": "e.battiloro1@gmail.com",
|
||||
"field:phone_4c77": "+39 333 767 3262",
|
||||
"_context": {
|
||||
"activation": {
|
||||
"id": "3fd865e1-f44a-49d2-ae29-19cf77ee488a"
|
||||
},
|
||||
"configuration": {
|
||||
"id": "a976f18c-fa86-495d-be1e-676df188eeae"
|
||||
},
|
||||
"app": {
|
||||
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
|
||||
},
|
||||
"action": {
|
||||
"id": "152db4d7-5263-40c4-be2b-1c81476318b7"
|
||||
},
|
||||
"trigger": {
|
||||
"key": "wix_form_app-form_submitted"
|
||||
}
|
||||
},
|
||||
"field:gclid": "",
|
||||
"formFieldMask": [
|
||||
"field:angebot_auswaehlen",
|
||||
"field:date_picker_a7c8",
|
||||
"field:date_picker_7e65",
|
||||
"field:number_7cf5",
|
||||
"field:anzahl_kinder",
|
||||
"field:alter_kind_3",
|
||||
"field:alter_kind_25",
|
||||
"field:alter_kind_4",
|
||||
"field:alter_kind_5",
|
||||
"field:alter_kind_6",
|
||||
"field:alter_kind_7",
|
||||
"field:alter_kind_8",
|
||||
"field:alter_kind_9",
|
||||
"field:alter_kind_10",
|
||||
"field:alter_kind_11",
|
||||
"field:anrede",
|
||||
"field:first_name_abae",
|
||||
"field:last_name_d97c",
|
||||
"field:email_5139",
|
||||
"field:phone_4c77",
|
||||
"field:long_answer_3524",
|
||||
"field:form_field_5a7b",
|
||||
"field:utm_source",
|
||||
"field:utm_medium",
|
||||
"field:utm_campaign",
|
||||
"field:utm_term",
|
||||
"field:utm_content",
|
||||
"field:utm_term_id",
|
||||
"field:utm_content_id",
|
||||
"field:gad_source",
|
||||
"field:gad_campaignid",
|
||||
"field:gbraid",
|
||||
"field:gclid",
|
||||
"field:fbclid",
|
||||
"metaSiteId"
|
||||
],
|
||||
"field:alter_kind_4": "0",
|
||||
"contact": {
|
||||
"name": {
|
||||
"first": "Elena",
|
||||
"last": "Battiloro"
|
||||
},
|
||||
"email": "e.battiloro1@gmail.com",
|
||||
"locale": "it-it",
|
||||
"phones": [
|
||||
{
|
||||
"tag": "UNTAGGED",
|
||||
"formattedPhone": "+39 333 767 3262",
|
||||
"id": "7e5c8512-b88e-4cf0-8d0c-9ebe6b210924",
|
||||
"countryCode": "IT",
|
||||
"e164Phone": "+393337673262",
|
||||
"primary": true,
|
||||
"phone": "333 767 3262"
|
||||
}
|
||||
],
|
||||
"contactId": "b9d47825-9f84-4ae7-873c-d169851b5888",
|
||||
"emails": [
|
||||
{
|
||||
"id": "c5609c67-5eba-4068-ab21-8a2ab9a09a27",
|
||||
"tag": "UNTAGGED",
|
||||
"email": "e.battiloro1@gmail.com",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"updatedDate": "2025-09-28T13:26:09.916Z",
|
||||
"phone": "+393337673262",
|
||||
"createdDate": "2025-08-08T13:05:23.733Z"
|
||||
},
|
||||
"submissionId": "02fbc71c-745b-4c73-9cba-827d0958117a",
|
||||
"field:anzahl_kinder": "3",
|
||||
"field:alter_kind_25": "1",
|
||||
"field:first_name_abae": "Elena",
|
||||
"field:utm_content_id": "120232007764490196",
|
||||
"field:utm_campaign": "Conversions_Hotel_Bemelmans_ITA",
|
||||
"field:utm_term": "Cold_Traffic_Conversions_Hotel_Bemelmans_ITA",
|
||||
"contactId": "b9d47825-9f84-4ae7-873c-d169851b5888",
|
||||
"field:date_picker_a7c8": "2025-10-31",
|
||||
"field:angebot_auswaehlen": "Herbstferien - Familienzeit mit Dolomitenblick",
|
||||
"field:utm_content": "Grafik_4_Spätsommer_23.08-07.09_Landingpage_ITA",
|
||||
"field:last_name_d97c": "Battiloro",
|
||||
"submissionsLink": "https://manage.wix.app/forms/submissions/1dea821c-8168-4736-96e4-4b92e8b364cf/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F1dea821c-8168-4736-96e4-4b92e8b364cf%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
|
||||
"field:gbraid": "",
|
||||
"field:fbclid": "PAZXh0bgNhZW0BMABhZGlkAasmYBhk4DQBp02L46Rl1jAuccxsOaeFSv7WSFnP-MQCsOrz9yDnKRH4hwZ7GEgxF9gy0_OF_aem_qSvrs6xsBkvTaI_Y9_hfnQ",
|
||||
"field:anrede": "Frau",
|
||||
"formId": "e084006b-ae83-4e4d-b2f5-074118cdb3b1"
|
||||
}
|
||||
},
|
||||
"origin_header": null,
|
||||
"all_headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "6920"
|
||||
}
|
||||
}
|
||||
250
logs/wix_test_data_20250929_154454.json
Normal file
250
logs/wix_test_data_20250929_154454.json
Normal file
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"timestamp": "2025-09-29T15:44:54.746579",
|
||||
"client_ip": "127.0.0.1",
|
||||
"headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "6920"
|
||||
},
|
||||
"data": {
|
||||
"data": {
|
||||
"formName": "Contact us",
|
||||
"submissions": [
|
||||
{
|
||||
"label": "Angebot auswählen",
|
||||
"value": "Herbstferien - Familienzeit mit Dolomitenblick"
|
||||
},
|
||||
{
|
||||
"label": "Anreisedatum",
|
||||
"value": "2025-10-31"
|
||||
},
|
||||
{
|
||||
"label": "Abreisedatum",
|
||||
"value": "2025-11-02"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Erwachsene",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Kinder",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"label": "Alter Kind 1",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"label": "Alter Kind 2",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"label": "Alter Kind 3",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"label": "Anrede",
|
||||
"value": "Frau"
|
||||
},
|
||||
{
|
||||
"label": "Vorname",
|
||||
"value": "Elena"
|
||||
},
|
||||
{
|
||||
"label": "Nachname",
|
||||
"value": "Battiloro"
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "e.battiloro1@gmail.com"
|
||||
},
|
||||
{
|
||||
"label": "Phone",
|
||||
"value": "+39 333 767 3262"
|
||||
},
|
||||
{
|
||||
"label": "Einwilligung Marketing",
|
||||
"value": "Non selezionato"
|
||||
},
|
||||
{
|
||||
"label": "utm_Source",
|
||||
"value": "ig"
|
||||
},
|
||||
{
|
||||
"label": "utm_Medium",
|
||||
"value": "Instagram_Stories"
|
||||
},
|
||||
{
|
||||
"label": "utm_Campaign",
|
||||
"value": "Conversions_Hotel_Bemelmans_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_Term",
|
||||
"value": "Cold_Traffic_Conversions_Hotel_Bemelmans_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_Content",
|
||||
"value": "Grafik_4_Spätsommer_23.08-07.09_Landingpage_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_term_id",
|
||||
"value": "120232007764490196"
|
||||
},
|
||||
{
|
||||
"label": "utm_content_id",
|
||||
"value": "120232007764490196"
|
||||
},
|
||||
{
|
||||
"label": "gad_source",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gad_campaignid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gbraid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gclid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "fbclid",
|
||||
"value": "PAZXh0bgNhZW0BMABhZGlkAasmYBhk4DQBp02L46Rl1jAuccxsOaeFSv7WSFnP-MQCsOrz9yDnKRH4hwZ7GEgxF9gy0_OF_aem_qSvrs6xsBkvTaI_Y9_hfnQ"
|
||||
}
|
||||
],
|
||||
"field:date_picker_7e65": "2025-11-02",
|
||||
"field:number_7cf5": "2",
|
||||
"field:utm_source": "ig",
|
||||
"submissionTime": "2025-09-28T13:26:07.938Z",
|
||||
"field:alter_kind_3": "3",
|
||||
"field:gad_source": "",
|
||||
"field:form_field_5a7b": "Non selezionato",
|
||||
"field:gad_campaignid": "",
|
||||
"field:utm_medium": "Instagram_Stories",
|
||||
"field:utm_term_id": "120232007764490196",
|
||||
"context": {
|
||||
"metaSiteId": "1dea821c-8168-4736-96e4-4b92e8b364cf",
|
||||
"activationId": "3fd865e1-f44a-49d2-ae29-19cf77ee488a"
|
||||
},
|
||||
"field:email_5139": "e.battiloro1@gmail.com",
|
||||
"field:phone_4c77": "+39 333 767 3262",
|
||||
"_context": {
|
||||
"activation": {
|
||||
"id": "3fd865e1-f44a-49d2-ae29-19cf77ee488a"
|
||||
},
|
||||
"configuration": {
|
||||
"id": "a976f18c-fa86-495d-be1e-676df188eeae"
|
||||
},
|
||||
"app": {
|
||||
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
|
||||
},
|
||||
"action": {
|
||||
"id": "152db4d7-5263-40c4-be2b-1c81476318b7"
|
||||
},
|
||||
"trigger": {
|
||||
"key": "wix_form_app-form_submitted"
|
||||
}
|
||||
},
|
||||
"field:gclid": "",
|
||||
"formFieldMask": [
|
||||
"field:angebot_auswaehlen",
|
||||
"field:date_picker_a7c8",
|
||||
"field:date_picker_7e65",
|
||||
"field:number_7cf5",
|
||||
"field:anzahl_kinder",
|
||||
"field:alter_kind_3",
|
||||
"field:alter_kind_25",
|
||||
"field:alter_kind_4",
|
||||
"field:alter_kind_5",
|
||||
"field:alter_kind_6",
|
||||
"field:alter_kind_7",
|
||||
"field:alter_kind_8",
|
||||
"field:alter_kind_9",
|
||||
"field:alter_kind_10",
|
||||
"field:alter_kind_11",
|
||||
"field:anrede",
|
||||
"field:first_name_abae",
|
||||
"field:last_name_d97c",
|
||||
"field:email_5139",
|
||||
"field:phone_4c77",
|
||||
"field:long_answer_3524",
|
||||
"field:form_field_5a7b",
|
||||
"field:utm_source",
|
||||
"field:utm_medium",
|
||||
"field:utm_campaign",
|
||||
"field:utm_term",
|
||||
"field:utm_content",
|
||||
"field:utm_term_id",
|
||||
"field:utm_content_id",
|
||||
"field:gad_source",
|
||||
"field:gad_campaignid",
|
||||
"field:gbraid",
|
||||
"field:gclid",
|
||||
"field:fbclid",
|
||||
"metaSiteId"
|
||||
],
|
||||
"field:alter_kind_4": "0",
|
||||
"contact": {
|
||||
"name": {
|
||||
"first": "Elena",
|
||||
"last": "Battiloro"
|
||||
},
|
||||
"email": "e.battiloro1@gmail.com",
|
||||
"locale": "it-it",
|
||||
"phones": [
|
||||
{
|
||||
"tag": "UNTAGGED",
|
||||
"formattedPhone": "+39 333 767 3262",
|
||||
"id": "7e5c8512-b88e-4cf0-8d0c-9ebe6b210924",
|
||||
"countryCode": "IT",
|
||||
"e164Phone": "+393337673262",
|
||||
"primary": true,
|
||||
"phone": "333 767 3262"
|
||||
}
|
||||
],
|
||||
"contactId": "b9d47825-9f84-4ae7-873c-d169851b5888",
|
||||
"emails": [
|
||||
{
|
||||
"id": "c5609c67-5eba-4068-ab21-8a2ab9a09a27",
|
||||
"tag": "UNTAGGED",
|
||||
"email": "e.battiloro1@gmail.com",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"updatedDate": "2025-09-28T13:26:09.916Z",
|
||||
"phone": "+393337673262",
|
||||
"createdDate": "2025-08-08T13:05:23.733Z"
|
||||
},
|
||||
"submissionId": "02fbc71c-745b-4c73-9cba-827d0958117a",
|
||||
"field:anzahl_kinder": "3",
|
||||
"field:alter_kind_25": "1",
|
||||
"field:first_name_abae": "Elena",
|
||||
"field:utm_content_id": "120232007764490196",
|
||||
"field:utm_campaign": "Conversions_Hotel_Bemelmans_ITA",
|
||||
"field:utm_term": "Cold_Traffic_Conversions_Hotel_Bemelmans_ITA",
|
||||
"contactId": "b9d47825-9f84-4ae7-873c-d169851b5888",
|
||||
"field:date_picker_a7c8": "2025-10-31",
|
||||
"field:angebot_auswaehlen": "Herbstferien - Familienzeit mit Dolomitenblick",
|
||||
"field:utm_content": "Grafik_4_Spätsommer_23.08-07.09_Landingpage_ITA",
|
||||
"field:last_name_d97c": "Battiloro",
|
||||
"submissionsLink": "https://manage.wix.app/forms/submissions/1dea821c-8168-4736-96e4-4b92e8b364cf/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F1dea821c-8168-4736-96e4-4b92e8b364cf%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
|
||||
"field:gbraid": "",
|
||||
"field:fbclid": "PAZXh0bgNhZW0BMABhZGlkAasmYBhk4DQBp02L46Rl1jAuccxsOaeFSv7WSFnP-MQCsOrz9yDnKRH4hwZ7GEgxF9gy0_OF_aem_qSvrs6xsBkvTaI_Y9_hfnQ",
|
||||
"field:anrede": "Frau",
|
||||
"formId": "e084006b-ae83-4e4d-b2f5-074118cdb3b1"
|
||||
}
|
||||
},
|
||||
"origin_header": null,
|
||||
"all_headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "6920"
|
||||
}
|
||||
}
|
||||
170
logs/wix_test_data_20250929_160843.json
Normal file
170
logs/wix_test_data_20250929_160843.json
Normal file
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"timestamp": "2025-09-29T16:08:43.177480",
|
||||
"client_ip": "127.0.0.1",
|
||||
"headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "4518"
|
||||
},
|
||||
"data": {
|
||||
"data": {
|
||||
"formName": "Contact us",
|
||||
"submissions": [
|
||||
{
|
||||
"label": "Anreisedatum",
|
||||
"value": "2026-01-17"
|
||||
},
|
||||
{
|
||||
"label": "Abreisedatum",
|
||||
"value": "2026-01-24"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Erwachsene",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Kinder",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"label": "Anrede",
|
||||
"value": "Herr"
|
||||
},
|
||||
{
|
||||
"label": "Vorname",
|
||||
"value": "Weislinger "
|
||||
},
|
||||
{
|
||||
"label": "Nachname",
|
||||
"value": "Alain "
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "alain-et-evelyne@hotmail.fr"
|
||||
},
|
||||
{
|
||||
"label": "Phone",
|
||||
"value": "+33 6 41 77 99 09"
|
||||
},
|
||||
{
|
||||
"label": "Einwilligung Marketing",
|
||||
"value": "Cochée"
|
||||
}
|
||||
],
|
||||
"field:date_picker_7e65": "2026-01-24",
|
||||
"field:number_7cf5": "2",
|
||||
"submissionTime": "2025-09-27T19:36:39.137Z",
|
||||
"field:form_field_5a7b": "Cochée",
|
||||
"context": {
|
||||
"metaSiteId": "7b28c2ce-1e20-4d07-9e86-73d822007e18",
|
||||
"activationId": "d59c463c-96e0-4742-b4f7-70b8f0431168"
|
||||
},
|
||||
"field:email_5139": "alain-et-evelyne@hotmail.fr",
|
||||
"field:phone_4c77": "+33 6 41 77 99 09",
|
||||
"_context": {
|
||||
"activation": {
|
||||
"id": "d59c463c-96e0-4742-b4f7-70b8f0431168"
|
||||
},
|
||||
"configuration": {
|
||||
"id": "483806f6-24ba-413f-9431-6b1ad9379f5c"
|
||||
},
|
||||
"app": {
|
||||
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
|
||||
},
|
||||
"action": {
|
||||
"id": "a85d9873-f8ed-426a-90b0-fb64a8e50406"
|
||||
},
|
||||
"trigger": {
|
||||
"key": "wix_form_app-form_submitted"
|
||||
}
|
||||
},
|
||||
"formFieldMask": [
|
||||
"field:angebot_auswaehlen",
|
||||
"field:date_picker_a7c8",
|
||||
"field:date_picker_7e65",
|
||||
"field:number_7cf5",
|
||||
"field:anzahl_kinder",
|
||||
"field:alter_kind_3",
|
||||
"field:alter_kind_25",
|
||||
"field:alter_kind_4",
|
||||
"field:alter_kind_5",
|
||||
"field:alter_kind_6",
|
||||
"field:alter_kind_7",
|
||||
"field:alter_kind_8",
|
||||
"field:alter_kind_9",
|
||||
"field:alter_kind_10",
|
||||
"field:alter_kind_11",
|
||||
"field:anrede",
|
||||
"field:first_name_abae",
|
||||
"field:last_name_d97c",
|
||||
"field:email_5139",
|
||||
"field:phone_4c77",
|
||||
"field:long_answer_3524",
|
||||
"field:form_field_5a7b",
|
||||
"field:utm_source",
|
||||
"field:utm_medium",
|
||||
"field:utm_campaign",
|
||||
"field:utm_term",
|
||||
"field:utm_content",
|
||||
"field:utm_term_id",
|
||||
"field:utm_content_id",
|
||||
"field:gad_source",
|
||||
"field:gad_campaignid",
|
||||
"field:gbraid",
|
||||
"field:gclid",
|
||||
"field:fbclid",
|
||||
"metaSiteId"
|
||||
],
|
||||
"contact": {
|
||||
"name": {
|
||||
"first": "Weislinger",
|
||||
"last": "Alain"
|
||||
},
|
||||
"email": "alain-et-evelyne@hotmail.fr",
|
||||
"locale": "de-de",
|
||||
"phones": [
|
||||
{
|
||||
"tag": "UNTAGGED",
|
||||
"formattedPhone": "+33 6 41 77 99 09",
|
||||
"id": "90ffc824-1fd7-4167-b29f-24a4b62a0773",
|
||||
"countryCode": "FR",
|
||||
"e164Phone": "+33641779909",
|
||||
"primary": true,
|
||||
"phone": "6 41 77 99 09"
|
||||
}
|
||||
],
|
||||
"contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7",
|
||||
"emails": [
|
||||
{
|
||||
"id": "2c071108-2410-4db8-99fa-b50b75a02493",
|
||||
"tag": "UNTAGGED",
|
||||
"email": "alain-et-evelyne@hotmail.fr",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"updatedDate": "2025-09-27T19:36:41.908Z",
|
||||
"phone": "+33641779909",
|
||||
"createdDate": "2025-09-27T19:36:41.054Z"
|
||||
},
|
||||
"submissionId": "6cfee967-69a8-454a-a10e-0aa03868ba6d",
|
||||
"field:anzahl_kinder": "0",
|
||||
"field:first_name_abae": "Weislinger ",
|
||||
"contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7",
|
||||
"field:date_picker_a7c8": "2026-01-17",
|
||||
"field:last_name_d97c": "Alain ",
|
||||
"submissionsLink": "https://manage.wix.app/forms/submissions/7b28c2ce-1e20-4d07-9e86-73d822007e18/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F7b28c2ce-1e20-4d07-9e86-73d822007e18%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
|
||||
"field:anrede": "Herr",
|
||||
"formId": "e084006b-ae83-4e4d-b2f5-074118cdb3b1"
|
||||
}
|
||||
},
|
||||
"origin_header": null,
|
||||
"all_headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "4518"
|
||||
}
|
||||
}
|
||||
170
logs/wix_test_data_20250929_162447.json
Normal file
170
logs/wix_test_data_20250929_162447.json
Normal file
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"timestamp": "2025-09-29T16:24:47.833595",
|
||||
"client_ip": "127.0.0.1",
|
||||
"headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "4518"
|
||||
},
|
||||
"data": {
|
||||
"data": {
|
||||
"formName": "Contact us",
|
||||
"submissions": [
|
||||
{
|
||||
"label": "Anreisedatum",
|
||||
"value": "2026-01-17"
|
||||
},
|
||||
{
|
||||
"label": "Abreisedatum",
|
||||
"value": "2026-01-24"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Erwachsene",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Kinder",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"label": "Anrede",
|
||||
"value": "Herr"
|
||||
},
|
||||
{
|
||||
"label": "Vorname",
|
||||
"value": "Weislinger "
|
||||
},
|
||||
{
|
||||
"label": "Nachname",
|
||||
"value": "Alain "
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "alain-et-evelyne@hotmail.fr"
|
||||
},
|
||||
{
|
||||
"label": "Phone",
|
||||
"value": "+33 6 41 77 99 09"
|
||||
},
|
||||
{
|
||||
"label": "Einwilligung Marketing",
|
||||
"value": "Cochée"
|
||||
}
|
||||
],
|
||||
"field:date_picker_7e65": "2026-01-24",
|
||||
"field:number_7cf5": "2",
|
||||
"submissionTime": "2025-09-27T19:36:39.137Z",
|
||||
"field:form_field_5a7b": "Cochée",
|
||||
"context": {
|
||||
"metaSiteId": "7b28c2ce-1e20-4d07-9e86-73d822007e18",
|
||||
"activationId": "d59c463c-96e0-4742-b4f7-70b8f0431168"
|
||||
},
|
||||
"field:email_5139": "alain-et-evelyne@hotmail.fr",
|
||||
"field:phone_4c77": "+33 6 41 77 99 09",
|
||||
"_context": {
|
||||
"activation": {
|
||||
"id": "d59c463c-96e0-4742-b4f7-70b8f0431168"
|
||||
},
|
||||
"configuration": {
|
||||
"id": "483806f6-24ba-413f-9431-6b1ad9379f5c"
|
||||
},
|
||||
"app": {
|
||||
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
|
||||
},
|
||||
"action": {
|
||||
"id": "a85d9873-f8ed-426a-90b0-fb64a8e50406"
|
||||
},
|
||||
"trigger": {
|
||||
"key": "wix_form_app-form_submitted"
|
||||
}
|
||||
},
|
||||
"formFieldMask": [
|
||||
"field:angebot_auswaehlen",
|
||||
"field:date_picker_a7c8",
|
||||
"field:date_picker_7e65",
|
||||
"field:number_7cf5",
|
||||
"field:anzahl_kinder",
|
||||
"field:alter_kind_3",
|
||||
"field:alter_kind_25",
|
||||
"field:alter_kind_4",
|
||||
"field:alter_kind_5",
|
||||
"field:alter_kind_6",
|
||||
"field:alter_kind_7",
|
||||
"field:alter_kind_8",
|
||||
"field:alter_kind_9",
|
||||
"field:alter_kind_10",
|
||||
"field:alter_kind_11",
|
||||
"field:anrede",
|
||||
"field:first_name_abae",
|
||||
"field:last_name_d97c",
|
||||
"field:email_5139",
|
||||
"field:phone_4c77",
|
||||
"field:long_answer_3524",
|
||||
"field:form_field_5a7b",
|
||||
"field:utm_source",
|
||||
"field:utm_medium",
|
||||
"field:utm_campaign",
|
||||
"field:utm_term",
|
||||
"field:utm_content",
|
||||
"field:utm_term_id",
|
||||
"field:utm_content_id",
|
||||
"field:gad_source",
|
||||
"field:gad_campaignid",
|
||||
"field:gbraid",
|
||||
"field:gclid",
|
||||
"field:fbclid",
|
||||
"metaSiteId"
|
||||
],
|
||||
"contact": {
|
||||
"name": {
|
||||
"first": "Weislinger",
|
||||
"last": "Alain"
|
||||
},
|
||||
"email": "alain-et-evelyne@hotmail.fr",
|
||||
"locale": "de-de",
|
||||
"phones": [
|
||||
{
|
||||
"tag": "UNTAGGED",
|
||||
"formattedPhone": "+33 6 41 77 99 09",
|
||||
"id": "90ffc824-1fd7-4167-b29f-24a4b62a0773",
|
||||
"countryCode": "FR",
|
||||
"e164Phone": "+33641779909",
|
||||
"primary": true,
|
||||
"phone": "6 41 77 99 09"
|
||||
}
|
||||
],
|
||||
"contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7",
|
||||
"emails": [
|
||||
{
|
||||
"id": "2c071108-2410-4db8-99fa-b50b75a02493",
|
||||
"tag": "UNTAGGED",
|
||||
"email": "alain-et-evelyne@hotmail.fr",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"updatedDate": "2025-09-27T19:36:41.908Z",
|
||||
"phone": "+33641779909",
|
||||
"createdDate": "2025-09-27T19:36:41.054Z"
|
||||
},
|
||||
"submissionId": "6cfee967-69a8-454a-a10e-0aa03868ba6d",
|
||||
"field:anzahl_kinder": "0",
|
||||
"field:first_name_abae": "Weislinger ",
|
||||
"contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7",
|
||||
"field:date_picker_a7c8": "2026-01-17",
|
||||
"field:last_name_d97c": "Alain ",
|
||||
"submissionsLink": "https://manage.wix.app/forms/submissions/7b28c2ce-1e20-4d07-9e86-73d822007e18/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F7b28c2ce-1e20-4d07-9e86-73d822007e18%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
|
||||
"field:anrede": "Herr",
|
||||
"formId": "e084006b-ae83-4e4d-b2f5-074118cdb3b1"
|
||||
}
|
||||
},
|
||||
"origin_header": null,
|
||||
"all_headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "4518"
|
||||
}
|
||||
}
|
||||
170
logs/wix_test_data_20250929_163212.json
Normal file
170
logs/wix_test_data_20250929_163212.json
Normal file
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"timestamp": "2025-09-29T16:32:12.776585",
|
||||
"client_ip": "127.0.0.1",
|
||||
"headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "4518"
|
||||
},
|
||||
"data": {
|
||||
"data": {
|
||||
"formName": "Contact us",
|
||||
"submissions": [
|
||||
{
|
||||
"label": "Anreisedatum",
|
||||
"value": "2026-01-17"
|
||||
},
|
||||
{
|
||||
"label": "Abreisedatum",
|
||||
"value": "2026-01-24"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Erwachsene",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Kinder",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"label": "Anrede",
|
||||
"value": "Herr"
|
||||
},
|
||||
{
|
||||
"label": "Vorname",
|
||||
"value": "Weislinger "
|
||||
},
|
||||
{
|
||||
"label": "Nachname",
|
||||
"value": "Alain "
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "alain-et-evelyne@hotmail.fr"
|
||||
},
|
||||
{
|
||||
"label": "Phone",
|
||||
"value": "+33 6 41 77 99 09"
|
||||
},
|
||||
{
|
||||
"label": "Einwilligung Marketing",
|
||||
"value": "Cochée"
|
||||
}
|
||||
],
|
||||
"field:date_picker_7e65": "2026-01-24",
|
||||
"field:number_7cf5": "2",
|
||||
"submissionTime": "2025-09-27T19:36:39.137Z",
|
||||
"field:form_field_5a7b": "Cochée",
|
||||
"context": {
|
||||
"metaSiteId": "7b28c2ce-1e20-4d07-9e86-73d822007e18",
|
||||
"activationId": "d59c463c-96e0-4742-b4f7-70b8f0431168"
|
||||
},
|
||||
"field:email_5139": "alain-et-evelyne@hotmail.fr",
|
||||
"field:phone_4c77": "+33 6 41 77 99 09",
|
||||
"_context": {
|
||||
"activation": {
|
||||
"id": "d59c463c-96e0-4742-b4f7-70b8f0431168"
|
||||
},
|
||||
"configuration": {
|
||||
"id": "483806f6-24ba-413f-9431-6b1ad9379f5c"
|
||||
},
|
||||
"app": {
|
||||
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
|
||||
},
|
||||
"action": {
|
||||
"id": "a85d9873-f8ed-426a-90b0-fb64a8e50406"
|
||||
},
|
||||
"trigger": {
|
||||
"key": "wix_form_app-form_submitted"
|
||||
}
|
||||
},
|
||||
"formFieldMask": [
|
||||
"field:angebot_auswaehlen",
|
||||
"field:date_picker_a7c8",
|
||||
"field:date_picker_7e65",
|
||||
"field:number_7cf5",
|
||||
"field:anzahl_kinder",
|
||||
"field:alter_kind_3",
|
||||
"field:alter_kind_25",
|
||||
"field:alter_kind_4",
|
||||
"field:alter_kind_5",
|
||||
"field:alter_kind_6",
|
||||
"field:alter_kind_7",
|
||||
"field:alter_kind_8",
|
||||
"field:alter_kind_9",
|
||||
"field:alter_kind_10",
|
||||
"field:alter_kind_11",
|
||||
"field:anrede",
|
||||
"field:first_name_abae",
|
||||
"field:last_name_d97c",
|
||||
"field:email_5139",
|
||||
"field:phone_4c77",
|
||||
"field:long_answer_3524",
|
||||
"field:form_field_5a7b",
|
||||
"field:utm_source",
|
||||
"field:utm_medium",
|
||||
"field:utm_campaign",
|
||||
"field:utm_term",
|
||||
"field:utm_content",
|
||||
"field:utm_term_id",
|
||||
"field:utm_content_id",
|
||||
"field:gad_source",
|
||||
"field:gad_campaignid",
|
||||
"field:gbraid",
|
||||
"field:gclid",
|
||||
"field:fbclid",
|
||||
"metaSiteId"
|
||||
],
|
||||
"contact": {
|
||||
"name": {
|
||||
"first": "Weislinger",
|
||||
"last": "Alain"
|
||||
},
|
||||
"email": "alain-et-evelyne@hotmail.fr",
|
||||
"locale": "de-de",
|
||||
"phones": [
|
||||
{
|
||||
"tag": "UNTAGGED",
|
||||
"formattedPhone": "+33 6 41 77 99 09",
|
||||
"id": "90ffc824-1fd7-4167-b29f-24a4b62a0773",
|
||||
"countryCode": "FR",
|
||||
"e164Phone": "+33641779909",
|
||||
"primary": true,
|
||||
"phone": "6 41 77 99 09"
|
||||
}
|
||||
],
|
||||
"contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7",
|
||||
"emails": [
|
||||
{
|
||||
"id": "2c071108-2410-4db8-99fa-b50b75a02493",
|
||||
"tag": "UNTAGGED",
|
||||
"email": "alain-et-evelyne@hotmail.fr",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"updatedDate": "2025-09-27T19:36:41.908Z",
|
||||
"phone": "+33641779909",
|
||||
"createdDate": "2025-09-27T19:36:41.054Z"
|
||||
},
|
||||
"submissionId": "6cfee967-69a8-454a-a10e-0aa03868ba6d",
|
||||
"field:anzahl_kinder": "0",
|
||||
"field:first_name_abae": "Weislinger ",
|
||||
"contactId": "250e24db-d41e-4f6e-835d-75acdf2ef2b7",
|
||||
"field:date_picker_a7c8": "2026-01-17",
|
||||
"field:last_name_d97c": "Alain ",
|
||||
"submissionsLink": "https://manage.wix.app/forms/submissions/7b28c2ce-1e20-4d07-9e86-73d822007e18/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F7b28c2ce-1e20-4d07-9e86-73d822007e18%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
|
||||
"field:anrede": "Herr",
|
||||
"formId": "e084006b-ae83-4e4d-b2f5-074118cdb3b1"
|
||||
}
|
||||
},
|
||||
"origin_header": null,
|
||||
"all_headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "4518"
|
||||
}
|
||||
}
|
||||
240
logs/wix_test_data_20250929_163449.json
Normal file
240
logs/wix_test_data_20250929_163449.json
Normal file
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"timestamp": "2025-09-29T16:34:49.785457",
|
||||
"client_ip": "127.0.0.1",
|
||||
"headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "6638"
|
||||
},
|
||||
"data": {
|
||||
"data": {
|
||||
"formName": "Contact us",
|
||||
"submissions": [
|
||||
{
|
||||
"label": "Angebot auswählen",
|
||||
"value": "Zimmer: Doppelzimmer"
|
||||
},
|
||||
{
|
||||
"label": "Anreisedatum",
|
||||
"value": "2025-10-03"
|
||||
},
|
||||
{
|
||||
"label": "Abreisedatum",
|
||||
"value": "2025-10-05"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Erwachsene",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"label": "Anzahl Kinder",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"label": "Alter Kind 1",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"label": "Anrede",
|
||||
"value": "Familie"
|
||||
},
|
||||
{
|
||||
"label": "Vorname",
|
||||
"value": "Miriana"
|
||||
},
|
||||
{
|
||||
"label": "Nachname",
|
||||
"value": "Darman"
|
||||
},
|
||||
{
|
||||
"label": "Email",
|
||||
"value": "miriana.m9@gmail.com"
|
||||
},
|
||||
{
|
||||
"label": "Phone",
|
||||
"value": "+39 348 443 0969"
|
||||
},
|
||||
{
|
||||
"label": "Einwilligung Marketing",
|
||||
"value": "Non selezionato"
|
||||
},
|
||||
{
|
||||
"label": "utm_Source",
|
||||
"value": "ig"
|
||||
},
|
||||
{
|
||||
"label": "utm_Medium",
|
||||
"value": "Instagram_Stories"
|
||||
},
|
||||
{
|
||||
"label": "utm_Campaign",
|
||||
"value": "Conversions_Hotel_Bemelmans_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_Term",
|
||||
"value": "Cold_Traffic_Conversions_Hotel_Bemelmans_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_Content",
|
||||
"value": "Grafik_4_Spätsommer_23.08-07.09_Landingpage_ITA"
|
||||
},
|
||||
{
|
||||
"label": "utm_term_id",
|
||||
"value": "120232007764490196"
|
||||
},
|
||||
{
|
||||
"label": "utm_content_id",
|
||||
"value": "120232007764490196"
|
||||
},
|
||||
{
|
||||
"label": "gad_source",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gad_campaignid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gbraid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "gclid",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"label": "fbclid",
|
||||
"value": "PAZXh0bgNhZW0BMABhZGlkAasmYBTNE3QBp1jWuJ9zIpfEGRJMP63fMAMI405yvG5EtH-OT0PxSkAbBJaudFHR6cMtkdHu_aem_fopaFtECyVPNW9fmWfEkyA"
|
||||
}
|
||||
],
|
||||
"field:date_picker_7e65": "2025-10-05",
|
||||
"field:number_7cf5": "2",
|
||||
"field:utm_source": "ig",
|
||||
"submissionTime": "2025-09-27T07:04:55.843Z",
|
||||
"field:alter_kind_3": "3",
|
||||
"field:gad_source": "",
|
||||
"field:form_field_5a7b": "Non selezionato",
|
||||
"field:gad_campaignid": "",
|
||||
"field:utm_medium": "Instagram_Stories",
|
||||
"field:utm_term_id": "120232007764490196",
|
||||
"context": {
|
||||
"metaSiteId": "1dea821c-8168-4736-96e4-4b92e8b364cf",
|
||||
"activationId": "d41b7796-dca2-40f1-8245-c2f26a096f19"
|
||||
},
|
||||
"field:email_5139": "miriana.m9@gmail.com",
|
||||
"field:phone_4c77": "+39 348 443 0969",
|
||||
"_context": {
|
||||
"activation": {
|
||||
"id": "d41b7796-dca2-40f1-8245-c2f26a096f19"
|
||||
},
|
||||
"configuration": {
|
||||
"id": "a976f18c-fa86-495d-be1e-676df188eeae"
|
||||
},
|
||||
"app": {
|
||||
"id": "225dd912-7dea-4738-8688-4b8c6955ffc2"
|
||||
},
|
||||
"action": {
|
||||
"id": "152db4d7-5263-40c4-be2b-1c81476318b7"
|
||||
},
|
||||
"trigger": {
|
||||
"key": "wix_form_app-form_submitted"
|
||||
}
|
||||
},
|
||||
"field:gclid": "",
|
||||
"formFieldMask": [
|
||||
"field:angebot_auswaehlen",
|
||||
"field:date_picker_a7c8",
|
||||
"field:date_picker_7e65",
|
||||
"field:number_7cf5",
|
||||
"field:anzahl_kinder",
|
||||
"field:alter_kind_3",
|
||||
"field:alter_kind_25",
|
||||
"field:alter_kind_4",
|
||||
"field:alter_kind_5",
|
||||
"field:alter_kind_6",
|
||||
"field:alter_kind_7",
|
||||
"field:alter_kind_8",
|
||||
"field:alter_kind_9",
|
||||
"field:alter_kind_10",
|
||||
"field:alter_kind_11",
|
||||
"field:anrede",
|
||||
"field:first_name_abae",
|
||||
"field:last_name_d97c",
|
||||
"field:email_5139",
|
||||
"field:phone_4c77",
|
||||
"field:long_answer_3524",
|
||||
"field:form_field_5a7b",
|
||||
"field:utm_source",
|
||||
"field:utm_medium",
|
||||
"field:utm_campaign",
|
||||
"field:utm_term",
|
||||
"field:utm_content",
|
||||
"field:utm_term_id",
|
||||
"field:utm_content_id",
|
||||
"field:gad_source",
|
||||
"field:gad_campaignid",
|
||||
"field:gbraid",
|
||||
"field:gclid",
|
||||
"field:fbclid",
|
||||
"metaSiteId"
|
||||
],
|
||||
"contact": {
|
||||
"name": {
|
||||
"first": "Miriana",
|
||||
"last": "Darman"
|
||||
},
|
||||
"email": "miriana.m9@gmail.com",
|
||||
"locale": "it-it",
|
||||
"phones": [
|
||||
{
|
||||
"tag": "UNTAGGED",
|
||||
"formattedPhone": "+39 348 443 0969",
|
||||
"id": "ac9d623e-6aaa-4022-856a-0dd64d0ff3fb",
|
||||
"countryCode": "IT",
|
||||
"e164Phone": "+393484430969",
|
||||
"primary": true,
|
||||
"phone": "348 443 0969"
|
||||
}
|
||||
],
|
||||
"contactId": "bcc29403-82ac-445a-be52-90a67180f16f",
|
||||
"emails": [
|
||||
{
|
||||
"id": "448de804-7353-46ed-9ae3-9c13ca521917",
|
||||
"tag": "UNTAGGED",
|
||||
"email": "miriana.m9@gmail.com",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"updatedDate": "2025-09-27T07:04:58.724Z",
|
||||
"phone": "+393484430969",
|
||||
"createdDate": "2025-09-27T07:04:57.752Z"
|
||||
},
|
||||
"submissionId": "3150614e-1b0a-47ba-a774-b0a0c71d8110",
|
||||
"field:anzahl_kinder": "1",
|
||||
"field:first_name_abae": "Miriana",
|
||||
"field:utm_content_id": "120232007764490196",
|
||||
"field:utm_campaign": "Conversions_Hotel_Bemelmans_ITA",
|
||||
"field:utm_term": "Cold_Traffic_Conversions_Hotel_Bemelmans_ITA",
|
||||
"contactId": "bcc29403-82ac-445a-be52-90a67180f16f",
|
||||
"field:date_picker_a7c8": "2025-10-03",
|
||||
"field:angebot_auswaehlen": "Zimmer: Doppelzimmer",
|
||||
"field:utm_content": "Grafik_4_Spätsommer_23.08-07.09_Landingpage_ITA",
|
||||
"field:last_name_d97c": "Darman",
|
||||
"submissionsLink": "https://manage.wix.app/forms/submissions/1dea821c-8168-4736-96e4-4b92e8b364cf/e084006b-ae83-4e4d-b2f5-074118cdb3b1?d=https%3A%2F%2Fmanage.wix.com%2Fdashboard%2F1dea821c-8168-4736-96e4-4b92e8b364cf%2Fwix-forms%2Fform%2Fe084006b-ae83-4e4d-b2f5-074118cdb3b1%2Fsubmissions&s=true",
|
||||
"field:gbraid": "",
|
||||
"field:fbclid": "PAZXh0bgNhZW0BMABhZGlkAasmYBTNE3QBp1jWuJ9zIpfEGRJMP63fMAMI405yvG5EtH-OT0PxSkAbBJaudFHR6cMtkdHu_aem_fopaFtECyVPNW9fmWfEkyA",
|
||||
"field:anrede": "Familie",
|
||||
"formId": "e084006b-ae83-4e4d-b2f5-074118cdb3b1"
|
||||
}
|
||||
},
|
||||
"origin_header": null,
|
||||
"all_headers": {
|
||||
"host": "localhost:8080",
|
||||
"content-type": "application/json",
|
||||
"user-agent": "insomnia/2023.5.8",
|
||||
"accept": "*/*",
|
||||
"content-length": "6638"
|
||||
}
|
||||
}
|
||||
42
output.xml
42
output.xml
@@ -1,13 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OTA_ResRetrieveRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="7.000">
|
||||
<ReservationsList>
|
||||
<HotelReservation CreateDateTime="2025-09-25T13:33:19.275224+00:00" ResStatus="Requested" RoomStayReservation="true">
|
||||
<UniqueID Type="14" ID="6b34fe24ac2ff811"/>
|
||||
<HotelReservation CreateDateTime="2025-09-29T12:08:55.313540+00:00" ResStatus="Requested" RoomStayReservation="true">
|
||||
<UniqueID Type="14" ID="e084006b-ae83-4e4d-b2f5-074118cdb3b1"/>
|
||||
<RoomStays>
|
||||
<RoomStay>
|
||||
<TimeSpan>
|
||||
<StartDateWindow EarliestDate="2024-10-01" LatestDate="2024-10-02"/>
|
||||
</TimeSpan>
|
||||
<GuestCounts>
|
||||
<GuestCount Count="2"/>
|
||||
<GuestCount Count="1" Age="3"/>
|
||||
<GuestCount Count="1" Age="0"/>
|
||||
<GuestCount Count="1" Age="1"/>
|
||||
</GuestCounts>
|
||||
<TimeSpan Start="2025-10-31" End="2025-11-02"/>
|
||||
</RoomStay>
|
||||
</RoomStays>
|
||||
<ResGuests>
|
||||
@@ -15,21 +19,14 @@
|
||||
<Profiles>
|
||||
<ProfileInfo>
|
||||
<Profile>
|
||||
<Customer Gender="Male" BirthDate="1980-01-01" Language="en">
|
||||
<Customer Language="it">
|
||||
<PersonName>
|
||||
<NamePrefix>Mr.</NamePrefix>
|
||||
<GivenName>John</GivenName>
|
||||
<Surname>Doe</Surname>
|
||||
<NamePrefix>Frau</NamePrefix>
|
||||
<GivenName>Elena</GivenName>
|
||||
<Surname>Battiloro</Surname>
|
||||
</PersonName>
|
||||
<Telephone PhoneTechType="5" PhoneNumber="+1234567890"/>
|
||||
<Telephone PhoneNumber="+0987654321"/>
|
||||
<Email Remark="newsletter:yes">john.doe@example.com</Email>
|
||||
<Address Remark="catalog:no">
|
||||
<AddressLine>123 Main Street</AddressLine>
|
||||
<CityName>Anytown</CityName>
|
||||
<PostalCode>12345</PostalCode>
|
||||
<CountryName Code="US"/>
|
||||
</Address>
|
||||
<Telephone PhoneTechType="5" PhoneNumber="+393337673262"/>
|
||||
<Email Remark="newsletter:no">e.battiloro1@gmail.com</Email>
|
||||
</Customer>
|
||||
</Profile>
|
||||
</ProfileInfo>
|
||||
@@ -38,16 +35,13 @@
|
||||
</ResGuests>
|
||||
<ResGlobalInfo>
|
||||
<Comments>
|
||||
<Comment Name="customer comment">
|
||||
<ListItem ListItem="1" Language="en">Landing page comment</ListItem>
|
||||
<Text>This is a sample comment.</Text>
|
||||
</Comment>
|
||||
<Comment Name="additional info">
|
||||
<Text>This is a special request comment.</Text>
|
||||
<ListItem ListItem="1" Language="it">Herbstferien - Familienzeit mit Dolomitenblick</ListItem>
|
||||
<Text>Angebot/Offerta</Text>
|
||||
</Comment>
|
||||
</Comments>
|
||||
<HotelReservationIDs>
|
||||
<HotelReservationID ResID_Type="13" ResID_SourceContext="99tales"/>
|
||||
<HotelReservationID ResID_Type="13" ResID_Value="PAZXh0bgNhZW0BMABhZGlkAasmYBhk4DQBp02L46Rl1jAuccxsOaeFSv7WSFnP-MQCsOrz9yDnKRH4hwZ7GEgxF9gy0_OF_aem_qSvrs6xsBkvTaI_Y9_hfnQ" ResID_SourceContext="99tales"/>
|
||||
</HotelReservationIDs>
|
||||
<BasicPropertyInfo HotelCode="123" HotelName="Frangart Inn"/>
|
||||
</ResGlobalInfo>
|
||||
|
||||
@@ -4,15 +4,24 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "alpine-bits-python-server"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
description = "Alpine Bits Python Server implementation"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"aiosqlite>=0.21.0",
|
||||
"annotatedyaml>=1.0.0",
|
||||
"dotenv>=0.9.9",
|
||||
"fastapi>=0.117.1",
|
||||
"generateds>=2.44.3",
|
||||
"lxml>=6.0.1",
|
||||
"pytest>=8.4.2",
|
||||
"redis>=6.4.0",
|
||||
"ruff>=0.13.1",
|
||||
"slowapi>=0.1.9",
|
||||
"sqlalchemy>=2.0.43",
|
||||
"uvicorn>=0.37.0",
|
||||
"voluptuous>=0.15.2",
|
||||
"xsdata-pydantic[cli,lxml,soap]>=24.5",
|
||||
"xsdata[cli,lxml,soap]>=25.7",
|
||||
]
|
||||
@@ -28,4 +37,4 @@ testpaths = ["test"]
|
||||
pythonpath = ["src"]
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src", "test"]
|
||||
src = ["src", "test"]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Entry point for alpine_bits_python package."""
|
||||
|
||||
from .main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
print("running test main")
|
||||
main()
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Union, Optional, Any, TypeVar
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
from alpine_bits_python.db import Customer, Reservation
|
||||
|
||||
# Import the generated classes
|
||||
from .generated.alpinebits import OtaHotelResNotifRq, OtaResRetrieveRs, CommentName2
|
||||
from .generated.alpinebits import HotelReservationResStatus, OtaHotelResNotifRq, OtaResRetrieveRs, CommentName2, UniqueIdType2
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_LOGGER.setLevel(logging.INFO)
|
||||
|
||||
# Define type aliases for the two Customer types
|
||||
NotifCustomer = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGuests.ResGuest.Profiles.ProfileInfo.Profile.Customer
|
||||
@@ -15,10 +24,26 @@ NotifHotelReservationId = OtaHotelResNotifRq.HotelReservations.HotelReservation.
|
||||
RetrieveHotelReservationId = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.HotelReservationIds.HotelReservationId
|
||||
|
||||
# Define type aliases for Comments types
|
||||
NotifComments = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments
|
||||
RetrieveComments = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments
|
||||
NotifComment = OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments.Comment
|
||||
RetrieveComment = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments.Comment
|
||||
NotifComments = (
|
||||
OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments
|
||||
)
|
||||
RetrieveComments = (
|
||||
OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments
|
||||
)
|
||||
NotifComment = (
|
||||
OtaHotelResNotifRq.HotelReservations.HotelReservation.ResGlobalInfo.Comments.Comment
|
||||
)
|
||||
RetrieveComment = (
|
||||
OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.Comments.Comment
|
||||
)
|
||||
|
||||
# type aliases for GuestCounts
|
||||
NotifGuestCounts = (
|
||||
OtaHotelResNotifRq.HotelReservations.HotelReservation.RoomStays.RoomStay.GuestCounts
|
||||
)
|
||||
RetrieveGuestCounts = (
|
||||
OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.GuestCounts
|
||||
)
|
||||
|
||||
|
||||
# phonetechtype enum 1,3,5 voice, fax, mobile
|
||||
@@ -31,7 +56,14 @@ class PhoneTechType(Enum):
|
||||
# Enum to specify which OTA message type to use
|
||||
class OtaMessageType(Enum):
|
||||
NOTIF = "notification" # For OtaHotelResNotifRq
|
||||
RETRIEVE = "retrieve" # For OtaResRetrieveRs
|
||||
RETRIEVE = "retrieve" # For OtaResRetrieveRs
|
||||
|
||||
|
||||
@dataclass
|
||||
class KidsAgeData:
|
||||
"""Data class to hold information about children's ages."""
|
||||
|
||||
ages: list[int]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -65,6 +97,63 @@ class CustomerData:
|
||||
self.phone_numbers = []
|
||||
|
||||
|
||||
class GuestCountsFactory:
|
||||
@staticmethod
|
||||
def create_notif_guest_counts(
|
||||
adults: int, kids: Optional[list[int]] = None
|
||||
) -> NotifGuestCounts:
|
||||
"""
|
||||
Create a GuestCounts object for OtaHotelResNotifRq.
|
||||
:param adults: Number of adults
|
||||
:param kids: List of ages for each kid (optional)
|
||||
:return: GuestCounts instance
|
||||
"""
|
||||
return GuestCountsFactory._create_guest_counts(adults, kids, NotifGuestCounts)
|
||||
|
||||
@staticmethod
|
||||
def create_retrieve_guest_counts(
|
||||
adults: int, kids: Optional[list[int]] = None
|
||||
) -> RetrieveGuestCounts:
|
||||
"""
|
||||
Create a GuestCounts object for OtaResRetrieveRs.
|
||||
:param adults: Number of adults
|
||||
:param kids: List of ages for each kid (optional)
|
||||
:return: GuestCounts instance
|
||||
"""
|
||||
return GuestCountsFactory._create_guest_counts(
|
||||
adults, kids, RetrieveGuestCounts
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_guest_counts(
|
||||
adults: int, kids: Optional[list[int]], guest_counts_class: type
|
||||
) -> Any:
|
||||
"""
|
||||
Internal method to create a GuestCounts object of the specified type.
|
||||
:param adults: Number of adults
|
||||
:param kids: List of ages for each kid (optional)
|
||||
:param guest_counts_class: The GuestCounts class to instantiate
|
||||
:return: GuestCounts instance
|
||||
"""
|
||||
GuestCount = guest_counts_class.GuestCount
|
||||
guest_count_list = []
|
||||
if adults > 0:
|
||||
guest_count_list.append(GuestCount(count=str(adults)))
|
||||
if kids:
|
||||
# create a dict with amount of kids for each age
|
||||
age_count = {}
|
||||
|
||||
for age in kids:
|
||||
if age in age_count:
|
||||
age_count[age] += 1
|
||||
else:
|
||||
age_count[age] = 1
|
||||
|
||||
for age, count in age_count.items():
|
||||
guest_count_list.append(GuestCount(count=str(count), age=str(age)))
|
||||
return guest_counts_class(guest_count=guest_count_list)
|
||||
|
||||
|
||||
class CustomerFactory:
|
||||
"""Factory class to create Customer instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||
|
||||
@@ -294,9 +383,10 @@ class HotelReservationIdFactory:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass
|
||||
class CommentListItemData:
|
||||
"""Simple data class to hold comment list item information."""
|
||||
|
||||
value: str # The text content of the list item
|
||||
list_item: str # Numeric identifier (pattern: [0-9]+)
|
||||
language: str # Two-letter language code (pattern: [a-z][a-z])
|
||||
@@ -305,6 +395,7 @@ class CommentListItemData:
|
||||
@dataclass
|
||||
class CommentData:
|
||||
"""Simple data class to hold comment information without nested type constraints."""
|
||||
|
||||
name: CommentName2 # Required: "included services", "customer comment", "additional info"
|
||||
text: Optional[str] = None # Optional text content
|
||||
list_items: list[CommentListItemData] = None # Optional list items
|
||||
@@ -317,6 +408,7 @@ class CommentData:
|
||||
@dataclass
|
||||
class CommentsData:
|
||||
"""Simple data class to hold multiple comments (1-3 max)."""
|
||||
|
||||
comments: list[CommentData] = None # 1-3 comments maximum
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -326,78 +418,80 @@ class CommentsData:
|
||||
|
||||
class CommentFactory:
|
||||
"""Factory class to create Comment instances for both OtaHotelResNotifRq and OtaResRetrieveRs."""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_notif_comments(data: CommentsData) -> NotifComments:
|
||||
"""Create Comments for OtaHotelResNotifRq."""
|
||||
return CommentFactory._create_comments(NotifComments, NotifComment, data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_retrieve_comments(data: CommentsData) -> RetrieveComments:
|
||||
"""Create Comments for OtaResRetrieveRs."""
|
||||
return CommentFactory._create_comments(RetrieveComments, RetrieveComment, data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _create_comments(comments_class: type, comment_class: type, data: CommentsData) -> Any:
|
||||
def _create_comments(
|
||||
comments_class: type[RetrieveComments] | type[NotifComments], comment_class: type[RetrieveComment] | type[NotifComment], data: CommentsData
|
||||
) -> Any:
|
||||
"""Internal method to create comments of the specified type."""
|
||||
|
||||
|
||||
comments_list = []
|
||||
for comment_data in data.comments:
|
||||
# Create list items
|
||||
list_items = []
|
||||
for item_data in comment_data.list_items:
|
||||
_LOGGER.info(f"Creating list item: value={item_data.value}, list_item={item_data.list_item}, language={item_data.language}")
|
||||
|
||||
list_item = comment_class.ListItem(
|
||||
value=item_data.value,
|
||||
list_item=item_data.list_item,
|
||||
language=item_data.language
|
||||
language=item_data.language,
|
||||
)
|
||||
list_items.append(list_item)
|
||||
|
||||
|
||||
# Create comment
|
||||
comment = comment_class(
|
||||
name=comment_data.name,
|
||||
text=comment_data.text,
|
||||
list_item=list_items
|
||||
name=comment_data.name, text=comment_data.text, list_item=list_items
|
||||
)
|
||||
comments_list.append(comment)
|
||||
|
||||
|
||||
# Create comments container
|
||||
return comments_class(comment=comments_list)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_notif_comments(comments: NotifComments) -> CommentsData:
|
||||
"""Convert NotifComments back to CommentsData."""
|
||||
return CommentFactory._comments_to_data(comments)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_retrieve_comments(comments: RetrieveComments) -> CommentsData:
|
||||
"""Convert RetrieveComments back to CommentsData."""
|
||||
return CommentFactory._comments_to_data(comments)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _comments_to_data(comments: Any) -> CommentsData:
|
||||
"""Internal method to convert any comments type to CommentsData."""
|
||||
|
||||
|
||||
comments_data_list = []
|
||||
for comment in comments.comment:
|
||||
# Extract list items
|
||||
list_items_data = []
|
||||
if comment.list_item:
|
||||
for list_item in comment.list_item:
|
||||
list_items_data.append(CommentListItemData(
|
||||
value=list_item.value,
|
||||
list_item=list_item.list_item,
|
||||
language=list_item.language
|
||||
))
|
||||
|
||||
list_items_data.append(
|
||||
CommentListItemData(
|
||||
value=list_item.value,
|
||||
list_item=list_item.list_item,
|
||||
language=list_item.language,
|
||||
)
|
||||
)
|
||||
|
||||
# Extract comment data
|
||||
comment_data = CommentData(
|
||||
name=comment.name,
|
||||
text=comment.text,
|
||||
list_items=list_items_data
|
||||
name=comment.name, text=comment.text, list_items=list_items_data
|
||||
)
|
||||
comments_data_list.append(comment_data)
|
||||
|
||||
|
||||
return CommentsData(comments=comments_data_list)
|
||||
|
||||
|
||||
@@ -467,16 +561,19 @@ class ResGuestFactory:
|
||||
|
||||
class AlpineBitsFactory:
|
||||
"""Unified factory class for creating AlpineBits objects with a simple interface."""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create(data: Union[CustomerData, HotelReservationIdData, CommentsData], message_type: OtaMessageType) -> Any:
|
||||
def create(
|
||||
data: Union[CustomerData, HotelReservationIdData, CommentsData],
|
||||
message_type: OtaMessageType,
|
||||
) -> Any:
|
||||
"""
|
||||
Create an AlpineBits object based on the data type and message type.
|
||||
|
||||
|
||||
Args:
|
||||
data: The data object (CustomerData, HotelReservationIdData, CommentsData, etc.)
|
||||
message_type: Whether to create for NOTIF or RETRIEVE message types
|
||||
|
||||
|
||||
Returns:
|
||||
The appropriate AlpineBits object based on the data type and message type
|
||||
"""
|
||||
@@ -485,31 +582,35 @@ class AlpineBitsFactory:
|
||||
return CustomerFactory.create_notif_customer(data)
|
||||
else:
|
||||
return CustomerFactory.create_retrieve_customer(data)
|
||||
|
||||
|
||||
elif isinstance(data, HotelReservationIdData):
|
||||
if message_type == OtaMessageType.NOTIF:
|
||||
return HotelReservationIdFactory.create_notif_hotel_reservation_id(data)
|
||||
else:
|
||||
return HotelReservationIdFactory.create_retrieve_hotel_reservation_id(data)
|
||||
|
||||
return HotelReservationIdFactory.create_retrieve_hotel_reservation_id(
|
||||
data
|
||||
)
|
||||
|
||||
elif isinstance(data, CommentsData):
|
||||
if message_type == OtaMessageType.NOTIF:
|
||||
return CommentFactory.create_notif_comments(data)
|
||||
else:
|
||||
return CommentFactory.create_retrieve_comments(data)
|
||||
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported data type: {type(data)}")
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_res_guests(customer_data: CustomerData, message_type: OtaMessageType) -> Union[NotifResGuests, RetrieveResGuests]:
|
||||
def create_res_guests(
|
||||
customer_data: CustomerData, message_type: OtaMessageType
|
||||
) -> Union[NotifResGuests, RetrieveResGuests]:
|
||||
"""
|
||||
Create a complete ResGuests structure with a primary customer.
|
||||
|
||||
|
||||
Args:
|
||||
customer_data: The customer data
|
||||
message_type: Whether to create for NOTIF or RETRIEVE message types
|
||||
|
||||
|
||||
Returns:
|
||||
The appropriate ResGuests object
|
||||
"""
|
||||
@@ -517,45 +618,217 @@ class AlpineBitsFactory:
|
||||
return ResGuestFactory.create_notif_res_guests(customer_data)
|
||||
else:
|
||||
return ResGuestFactory.create_retrieve_res_guests(customer_data)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def extract_data(obj: Any) -> Union[CustomerData, HotelReservationIdData, CommentsData]:
|
||||
def extract_data(
|
||||
obj: Any,
|
||||
) -> Union[CustomerData, HotelReservationIdData, CommentsData]:
|
||||
"""
|
||||
Extract data from an AlpineBits object back to a simple data class.
|
||||
|
||||
|
||||
Args:
|
||||
obj: The AlpineBits object to extract data from
|
||||
|
||||
|
||||
Returns:
|
||||
The appropriate data object
|
||||
"""
|
||||
# Check if it's a Customer object
|
||||
if hasattr(obj, 'person_name') and hasattr(obj.person_name, 'given_name'):
|
||||
if hasattr(obj, "person_name") and hasattr(obj.person_name, "given_name"):
|
||||
if isinstance(obj, NotifCustomer):
|
||||
return CustomerFactory.from_notif_customer(obj)
|
||||
elif isinstance(obj, RetrieveCustomer):
|
||||
return CustomerFactory.from_retrieve_customer(obj)
|
||||
|
||||
|
||||
# Check if it's a HotelReservationId object
|
||||
elif hasattr(obj, 'res_id_type'):
|
||||
elif hasattr(obj, "res_id_type"):
|
||||
if isinstance(obj, NotifHotelReservationId):
|
||||
return HotelReservationIdFactory.from_notif_hotel_reservation_id(obj)
|
||||
elif isinstance(obj, RetrieveHotelReservationId):
|
||||
return HotelReservationIdFactory.from_retrieve_hotel_reservation_id(obj)
|
||||
|
||||
|
||||
# Check if it's a Comments object
|
||||
elif hasattr(obj, 'comment'):
|
||||
elif hasattr(obj, "comment"):
|
||||
if isinstance(obj, NotifComments):
|
||||
return CommentFactory.from_notif_comments(obj)
|
||||
elif isinstance(obj, RetrieveComments):
|
||||
return CommentFactory.from_retrieve_comments(obj)
|
||||
|
||||
|
||||
# Check if it's a ResGuests object
|
||||
elif hasattr(obj, 'res_guest'):
|
||||
elif hasattr(obj, "res_guest"):
|
||||
return ResGuestFactory.extract_primary_customer(obj)
|
||||
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported object type: {type(obj)}")
|
||||
|
||||
|
||||
def create_xml_from_db(list: list[Tuple[Reservation, Customer]]):
|
||||
""" Create RetrievedReservation XML from database entries.
|
||||
|
||||
list of pairs (Reservation, Customer)
|
||||
"""
|
||||
|
||||
reservations_list = []
|
||||
|
||||
for reservation, customer in list:
|
||||
_LOGGER.info(f"Creating XML for reservation {reservation.form_id} and customer {customer.given_name}")
|
||||
|
||||
try:
|
||||
|
||||
phone_numbers = [(customer.phone, PhoneTechType.MOBILE)] if customer.phone is not None else []
|
||||
customer_data = CustomerData(
|
||||
given_name=customer.given_name,
|
||||
surname=customer.surname,
|
||||
name_prefix=customer.name_prefix,
|
||||
name_title=customer.name_title,
|
||||
phone_numbers=phone_numbers,
|
||||
email_address=customer.email_address,
|
||||
email_newsletter=customer.email_newsletter,
|
||||
address_line=customer.address_line,
|
||||
city_name=customer.city_name,
|
||||
postal_code=customer.postal_code,
|
||||
country_code=customer.country_code,
|
||||
address_catalog=customer.address_catalog,
|
||||
gender=customer.gender,
|
||||
birth_date=customer.birth_date,
|
||||
language=customer.language,
|
||||
)
|
||||
alpine_bits_factory = AlpineBitsFactory()
|
||||
res_guests = alpine_bits_factory.create_res_guests(
|
||||
customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
# Guest counts
|
||||
children_ages = [int(a) for a in reservation.children_ages.split(",") if a]
|
||||
guest_counts = GuestCountsFactory.create_retrieve_guest_counts(
|
||||
reservation.num_adults, children_ages
|
||||
)
|
||||
|
||||
|
||||
unique_id_string = reservation.form_id
|
||||
|
||||
|
||||
if len(unique_id_string) > 32:
|
||||
unique_id_string = unique_id_string[:32] # Truncate to 32 characters
|
||||
|
||||
# UniqueID
|
||||
unique_id = OtaResRetrieveRs.ReservationsList.HotelReservation.UniqueId(
|
||||
type_value=UniqueIdType2.VALUE_14, id=unique_id_string
|
||||
)
|
||||
|
||||
# TimeSpan
|
||||
time_span = OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.TimeSpan(
|
||||
start=reservation.start_date.isoformat() if reservation.start_date else None,
|
||||
end=reservation.end_date.isoformat() if reservation.end_date else None,
|
||||
)
|
||||
room_stay = (
|
||||
OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay(
|
||||
time_span=time_span,
|
||||
guest_counts=guest_counts,
|
||||
)
|
||||
)
|
||||
room_stays = OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays(
|
||||
room_stay=[room_stay],
|
||||
)
|
||||
hotel_res_id_data = HotelReservationIdData(
|
||||
res_id_type="13",
|
||||
res_id_value=reservation.fbclid or reservation.gclid,
|
||||
res_id_source=None,
|
||||
res_id_source_context="99tales",
|
||||
)
|
||||
|
||||
hotel_res_id = alpine_bits_factory.create(
|
||||
hotel_res_id_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
hotel_res_ids = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.HotelReservationIds(
|
||||
hotel_reservation_id=[hotel_res_id]
|
||||
)
|
||||
basic_property_info = OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.BasicPropertyInfo(
|
||||
hotel_code=reservation.hotel_code,
|
||||
hotel_name=reservation.hotel_name,
|
||||
)
|
||||
# Comments
|
||||
|
||||
offer_comment = None
|
||||
if reservation.offer is not None:
|
||||
offer_comment = CommentData(
|
||||
name=CommentName2.ADDITIONAL_INFO,
|
||||
text="Angebot/Offerta",
|
||||
list_items=[
|
||||
CommentListItemData(
|
||||
value=reservation.offer,
|
||||
language=customer.language,
|
||||
list_item="1",
|
||||
)
|
||||
],
|
||||
)
|
||||
comment = None
|
||||
if reservation.user_comment:
|
||||
comment = CommentData(
|
||||
name=CommentName2.CUSTOMER_COMMENT,
|
||||
text=reservation.user_comment,
|
||||
list_items=[
|
||||
CommentListItemData(
|
||||
value="Landing page comment",
|
||||
language=customer.language,
|
||||
list_item="1",
|
||||
)
|
||||
],
|
||||
)
|
||||
comments = [offer_comment, comment]
|
||||
|
||||
# filter out None comments
|
||||
comments = [c for c in comments if c is not None]
|
||||
|
||||
comments_xml = None
|
||||
if comments:
|
||||
|
||||
for c in comments:
|
||||
_LOGGER.info(f"Creating comment: name={c.name}, text={c.text}, list_items={len(c.list_items)}")
|
||||
|
||||
comments_data = CommentsData(comments=comments)
|
||||
comments_xml = alpine_bits_factory.create(comments_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
res_global_info = (
|
||||
OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo(
|
||||
hotel_reservation_ids=hotel_res_ids,
|
||||
basic_property_info=basic_property_info,
|
||||
comments=comments_xml,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
hotel_reservation = OtaResRetrieveRs.ReservationsList.HotelReservation(
|
||||
create_date_time=datetime.now(timezone.utc).isoformat(),
|
||||
res_status=HotelReservationResStatus.REQUESTED,
|
||||
room_stay_reservation="true",
|
||||
unique_id=unique_id,
|
||||
room_stays=room_stays,
|
||||
res_guests=res_guests,
|
||||
res_global_info=res_global_info,
|
||||
)
|
||||
|
||||
reservations_list.append(hotel_reservation)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error creating XML for reservation {reservation.form_id} and customer {customer.given_name}: {e}")
|
||||
|
||||
retrieved_reservations = OtaResRetrieveRs.ReservationsList(
|
||||
hotel_reservation=reservations_list
|
||||
)
|
||||
|
||||
ota_res_retrieve_rs = OtaResRetrieveRs(
|
||||
version="7.000", success="", reservations_list=retrieved_reservations
|
||||
)
|
||||
|
||||
try:
|
||||
ota_res_retrieve_rs.model_validate(ota_res_retrieve_rs.model_dump())
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Validation error: {e}")
|
||||
raise
|
||||
|
||||
return ota_res_retrieve_rs
|
||||
|
||||
|
||||
|
||||
# Usage examples
|
||||
@@ -671,70 +944,74 @@ if __name__ == "__main__":
|
||||
|
||||
# Verify roundtrip conversion
|
||||
print("Roundtrip conversion successful:", customer_data == extracted_data)
|
||||
|
||||
|
||||
print("\n--- Unified AlpineBitsFactory Examples ---")
|
||||
|
||||
|
||||
# Much simpler approach - single factory with enum parameter!
|
||||
print("=== Customer Creation ===")
|
||||
notif_customer = AlpineBitsFactory.create(customer_data, OtaMessageType.NOTIF)
|
||||
retrieve_customer = AlpineBitsFactory.create(customer_data, OtaMessageType.RETRIEVE)
|
||||
print("Created customers using unified factory")
|
||||
|
||||
|
||||
print("=== HotelReservationId Creation ===")
|
||||
reservation_id_data = HotelReservationIdData(
|
||||
res_id_type="123",
|
||||
res_id_value="RESERVATION-456",
|
||||
res_id_source="HOTEL_SYSTEM"
|
||||
res_id_type="123", res_id_value="RESERVATION-456", res_id_source="HOTEL_SYSTEM"
|
||||
)
|
||||
notif_res_id = AlpineBitsFactory.create(reservation_id_data, OtaMessageType.NOTIF)
|
||||
retrieve_res_id = AlpineBitsFactory.create(reservation_id_data, OtaMessageType.RETRIEVE)
|
||||
retrieve_res_id = AlpineBitsFactory.create(
|
||||
reservation_id_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
print("Created reservation IDs using unified factory")
|
||||
|
||||
|
||||
print("=== Comments Creation ===")
|
||||
comments_data = CommentsData(comments=[
|
||||
CommentData(
|
||||
name=CommentName2.CUSTOMER_COMMENT,
|
||||
text="This is a customer comment about the reservation",
|
||||
list_items=[
|
||||
CommentListItemData(
|
||||
value="Special dietary requirements: vegetarian",
|
||||
list_item="1",
|
||||
language="en"
|
||||
),
|
||||
CommentListItemData(
|
||||
value="Late arrival expected",
|
||||
list_item="2",
|
||||
language="en"
|
||||
)
|
||||
]
|
||||
),
|
||||
CommentData(
|
||||
name=CommentName2.ADDITIONAL_INFO,
|
||||
text="Additional information about the stay"
|
||||
)
|
||||
])
|
||||
comments_data = CommentsData(
|
||||
comments=[
|
||||
CommentData(
|
||||
name=CommentName2.CUSTOMER_COMMENT,
|
||||
text="This is a customer comment about the reservation",
|
||||
list_items=[
|
||||
CommentListItemData(
|
||||
value="Special dietary requirements: vegetarian",
|
||||
list_item="1",
|
||||
language="en",
|
||||
),
|
||||
CommentListItemData(
|
||||
value="Late arrival expected", list_item="2", language="en"
|
||||
),
|
||||
],
|
||||
),
|
||||
CommentData(
|
||||
name=CommentName2.ADDITIONAL_INFO,
|
||||
text="Additional information about the stay",
|
||||
),
|
||||
]
|
||||
)
|
||||
notif_comments = AlpineBitsFactory.create(comments_data, OtaMessageType.NOTIF)
|
||||
retrieve_comments = AlpineBitsFactory.create(comments_data, OtaMessageType.RETRIEVE)
|
||||
print("Created comments using unified factory")
|
||||
|
||||
|
||||
print("=== ResGuests Creation ===")
|
||||
notif_res_guests = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.NOTIF)
|
||||
retrieve_res_guests = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.RETRIEVE)
|
||||
notif_res_guests = AlpineBitsFactory.create_res_guests(
|
||||
customer_data, OtaMessageType.NOTIF
|
||||
)
|
||||
retrieve_res_guests = AlpineBitsFactory.create_res_guests(
|
||||
customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
print("Created ResGuests using unified factory")
|
||||
|
||||
|
||||
print("=== Data Extraction ===")
|
||||
# Extract data back using unified interface
|
||||
extracted_customer_data = AlpineBitsFactory.extract_data(notif_customer)
|
||||
extracted_res_id_data = AlpineBitsFactory.extract_data(notif_res_id)
|
||||
extracted_comments_data = AlpineBitsFactory.extract_data(retrieve_comments)
|
||||
extracted_from_res_guests = AlpineBitsFactory.extract_data(retrieve_res_guests)
|
||||
|
||||
|
||||
print("Data extraction successful:")
|
||||
print("- Customer roundtrip:", customer_data == extracted_customer_data)
|
||||
print("- ReservationId roundtrip:", reservation_id_data == extracted_res_id_data)
|
||||
print("- Comments roundtrip:", comments_data == extracted_comments_data)
|
||||
print("- ResGuests roundtrip:", customer_data == extracted_from_res_guests)
|
||||
|
||||
|
||||
print("\n--- Comparison with old approach ---")
|
||||
print("Old way required multiple imports and knowing specific factory methods")
|
||||
print("New way: single import, single factory, enum parameter to specify type!")
|
||||
@@ -7,65 +7,98 @@ handshaking functionality with configurable supported actions and capabilities.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import difflib
|
||||
import json
|
||||
import inspect
|
||||
import re
|
||||
from typing import Dict, List, Optional, Any, Union, Tuple, Type
|
||||
from xml.etree import ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, IntEnum
|
||||
|
||||
from .generated.alpinebits import OtaPingRq, OtaPingRs, WarningStatus
|
||||
from alpine_bits_python.alpine_bits_helpers import PhoneTechType, create_xml_from_db
|
||||
|
||||
|
||||
|
||||
from .generated.alpinebits import OtaPingRq, OtaPingRs, WarningStatus, OtaReadRq
|
||||
from xsdata_pydantic.bindings import XmlSerializer
|
||||
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||
from abc import ABC, abstractmethod
|
||||
from xsdata_pydantic.bindings import XmlParser
|
||||
import logging
|
||||
from .db import Reservation, Customer
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class HttpStatusCode(IntEnum):
|
||||
"""Allowed HTTP status codes for AlpineBits responses."""
|
||||
|
||||
OK = 200
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
INTERNAL_SERVER_ERROR = 500
|
||||
|
||||
|
||||
|
||||
class AlpineBitsActionName(Enum):
|
||||
"""Enum for AlpineBits action names with capability and request name mappings."""
|
||||
|
||||
|
||||
# Format: (capability_name, actual_request_name)
|
||||
OTA_PING = ("action_OTA_Ping", "OTA_Ping:Handshaking")
|
||||
OTA_READ = ("action_OTA_Read", "OTA_Read:GuestRequests")
|
||||
OTA_HOTEL_AVAIL_NOTIF = ("action_OTA_HotelAvailNotif", "OTA_HotelAvailNotif")
|
||||
OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS = ("action_OTA_HotelResNotif_GuestRequests",
|
||||
"OTA_HotelResNotif:GuestRequests")
|
||||
OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INVENTORY = ("action_OTA_HotelDescriptiveContentNotif_Inventory",
|
||||
"OTA_HotelDescriptiveContentNotif:Inventory")
|
||||
OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INFO = ("action_OTA_HotelDescriptiveContentNotif_Info",
|
||||
"OTA_HotelDescriptiveContentNotif:Info")
|
||||
OTA_HOTEL_DESCRIPTIVE_INFO_INVENTORY = ("action_OTA_HotelDescriptiveInfo_Inventory",
|
||||
"OTA_HotelDescriptiveInfo:Inventory")
|
||||
OTA_HOTEL_DESCRIPTIVE_INFO_INFO = ("action_OTA_HotelDescriptiveInfo_Info",
|
||||
"OTA_HotelDescriptiveInfo:Info")
|
||||
OTA_HOTEL_RATE_PLAN_NOTIF_RATE_PLANS = ("action_OTA_HotelRatePlanNotif_RatePlans",
|
||||
"OTA_HotelRatePlanNotif:RatePlans")
|
||||
OTA_HOTEL_RATE_PLAN_BASE_RATES = ("action_OTA_HotelRatePlan_BaseRates",
|
||||
"OTA_HotelRatePlan:BaseRates")
|
||||
|
||||
OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS = (
|
||||
"action_OTA_HotelResNotif_GuestRequests",
|
||||
"OTA_HotelResNotif:GuestRequests",
|
||||
)
|
||||
OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INVENTORY = (
|
||||
"action_OTA_HotelDescriptiveContentNotif_Inventory",
|
||||
"OTA_HotelDescriptiveContentNotif:Inventory",
|
||||
)
|
||||
OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INFO = (
|
||||
"action_OTA_HotelDescriptiveContentNotif_Info",
|
||||
"OTA_HotelDescriptiveContentNotif:Info",
|
||||
)
|
||||
OTA_HOTEL_DESCRIPTIVE_INFO_INVENTORY = (
|
||||
"action_OTA_HotelDescriptiveInfo_Inventory",
|
||||
"OTA_HotelDescriptiveInfo:Inventory",
|
||||
)
|
||||
OTA_HOTEL_DESCRIPTIVE_INFO_INFO = (
|
||||
"action_OTA_HotelDescriptiveInfo_Info",
|
||||
"OTA_HotelDescriptiveInfo:Info",
|
||||
)
|
||||
OTA_HOTEL_RATE_PLAN_NOTIF_RATE_PLANS = (
|
||||
"action_OTA_HotelRatePlanNotif_RatePlans",
|
||||
"OTA_HotelRatePlanNotif:RatePlans",
|
||||
)
|
||||
OTA_HOTEL_RATE_PLAN_BASE_RATES = (
|
||||
"action_OTA_HotelRatePlan_BaseRates",
|
||||
"OTA_HotelRatePlan:BaseRates",
|
||||
)
|
||||
|
||||
def __init__(self, capability_name: str, request_name: str):
|
||||
self.capability_name = capability_name
|
||||
self.request_name = request_name
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_by_capability_name(cls, capability_name: str) -> Optional['AlpineBitsActionName']:
|
||||
def get_by_capability_name(
|
||||
cls, capability_name: str
|
||||
) -> Optional["AlpineBitsActionName"]:
|
||||
"""Get action enum by capability name."""
|
||||
for action in cls:
|
||||
if action.capability_name == capability_name:
|
||||
return action
|
||||
return None
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_by_request_name(cls, request_name: str) -> Optional['AlpineBitsActionName']:
|
||||
def get_by_request_name(cls, request_name: str) -> Optional["AlpineBitsActionName"]:
|
||||
"""Get action enum by request name."""
|
||||
for action in cls:
|
||||
if action.request_name == request_name:
|
||||
@@ -75,22 +108,25 @@ class AlpineBitsActionName(Enum):
|
||||
|
||||
class Version(str, Enum):
|
||||
"""Enum for AlpineBits versions."""
|
||||
|
||||
V2024_10 = "2024-10"
|
||||
V2022_10 = "2022-10"
|
||||
# Add other versions as needed
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlpineBitsResponse:
|
||||
"""Response data structure for AlpineBits actions."""
|
||||
|
||||
xml_content: str
|
||||
status_code: HttpStatusCode = HttpStatusCode.OK
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate that status code is one of the allowed values."""
|
||||
if self.status_code not in [200, 400, 401, 500]:
|
||||
raise ValueError(f"Invalid status code {self.status_code}. Must be 200, 400, 401, or 500")
|
||||
raise ValueError(
|
||||
f"Invalid status code {self.status_code}. Must be 200, 400, 401, or 500"
|
||||
)
|
||||
|
||||
|
||||
# Abstract base class for AlpineBits Action
|
||||
@@ -98,20 +134,24 @@ class AlpineBitsAction(ABC):
|
||||
"""Abstract base class for handling AlpineBits actions."""
|
||||
|
||||
name: AlpineBitsActionName
|
||||
version: Version | list[Version] # list of versions in case action supports multiple versions
|
||||
|
||||
async def handle(self, action: str, request_xml: str, version: Version) -> AlpineBitsResponse:
|
||||
version: (
|
||||
Version | list[Version]
|
||||
) # list of versions in case action supports multiple versions
|
||||
|
||||
async def handle(
|
||||
self, action: str, request_xml: str, version: Version, dbsession=None, server_capabilities=None, username=None, password=None, config: Dict = None
|
||||
) -> AlpineBitsResponse:
|
||||
"""
|
||||
Handle the incoming request XML and return response XML.
|
||||
|
||||
|
||||
Default implementation returns "not implemented" error.
|
||||
Override this method in subclasses to provide actual functionality.
|
||||
|
||||
|
||||
Args:
|
||||
action: The action to perform (e.g., "OTA_PingRQ")
|
||||
request_xml: The XML request body as string
|
||||
version: The AlpineBits version
|
||||
|
||||
|
||||
Returns:
|
||||
AlpineBitsResponse with error or actual response
|
||||
"""
|
||||
@@ -121,7 +161,7 @@ class AlpineBitsAction(ABC):
|
||||
async def check_version_supported(self, version: Version) -> bool:
|
||||
"""
|
||||
Check if the action supports the given version.
|
||||
|
||||
|
||||
Args:
|
||||
version: The AlpineBits version to check
|
||||
Returns:
|
||||
@@ -130,103 +170,93 @@ class AlpineBitsAction(ABC):
|
||||
if isinstance(self.version, list):
|
||||
return version in self.version
|
||||
return version == self.version
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ServerCapabilities:
|
||||
"""
|
||||
Automatically discovers AlpineBitsAction implementations and generates capabilities.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.action_registry: Dict[str, Type[AlpineBitsAction]] = {}
|
||||
self._discover_actions()
|
||||
self.capability_dict = None
|
||||
|
||||
|
||||
def _discover_actions(self):
|
||||
"""Discover all AlpineBitsAction implementations in the current module."""
|
||||
current_module = inspect.getmodule(self)
|
||||
|
||||
|
||||
for name, obj in inspect.getmembers(current_module):
|
||||
if (inspect.isclass(obj) and
|
||||
issubclass(obj, AlpineBitsAction) and
|
||||
obj != AlpineBitsAction):
|
||||
|
||||
if (
|
||||
inspect.isclass(obj)
|
||||
and issubclass(obj, AlpineBitsAction)
|
||||
and obj != AlpineBitsAction
|
||||
):
|
||||
# Check if this action is actually implemented (not just returning default)
|
||||
if self._is_action_implemented(obj):
|
||||
action_instance = obj()
|
||||
if hasattr(action_instance, 'name'):
|
||||
if hasattr(action_instance, "name"):
|
||||
# Use capability name for the registry key
|
||||
self.action_registry[action_instance.name.capability_name] = obj
|
||||
|
||||
|
||||
def _is_action_implemented(self, action_class: Type[AlpineBitsAction]) -> bool:
|
||||
"""
|
||||
Check if an action is actually implemented or just uses the default behavior.
|
||||
This is a simple check - in practice, you might want more sophisticated detection.
|
||||
"""
|
||||
# Check if the class has overridden the handle method
|
||||
if 'handle' in action_class.__dict__:
|
||||
if "handle" in action_class.__dict__:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def create_capabilities_dict(self) -> None:
|
||||
"""
|
||||
Generate the capabilities dictionary based on discovered actions.
|
||||
|
||||
|
||||
"""
|
||||
versions_dict = {}
|
||||
|
||||
|
||||
for action_name, action_class in self.action_registry.items():
|
||||
action_instance = action_class()
|
||||
|
||||
|
||||
# Get supported versions for this action
|
||||
if isinstance(action_instance.version, list):
|
||||
supported_versions = action_instance.version
|
||||
else:
|
||||
supported_versions = [action_instance.version]
|
||||
|
||||
|
||||
# Add action to each supported version
|
||||
for version in supported_versions:
|
||||
version_str = version.value
|
||||
|
||||
|
||||
if version_str not in versions_dict:
|
||||
versions_dict[version_str] = {
|
||||
"version": version_str,
|
||||
"actions": []
|
||||
}
|
||||
|
||||
versions_dict[version_str] = {"version": version_str, "actions": []}
|
||||
|
||||
action_dict = {"action": action_name}
|
||||
|
||||
|
||||
# Add supports field if the action has custom supports
|
||||
if hasattr(action_instance, 'supports') and action_instance.supports:
|
||||
if hasattr(action_instance, "supports") and action_instance.supports:
|
||||
action_dict["supports"] = action_instance.supports
|
||||
|
||||
|
||||
versions_dict[version_str]["actions"].append(action_dict)
|
||||
|
||||
self.capability_dict = {"versions": list(versions_dict.values())}
|
||||
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_capabilities_dict(self) -> Dict:
|
||||
"""
|
||||
Get capabilities as a dictionary. Generates if not already created.
|
||||
"""
|
||||
|
||||
|
||||
if self.capability_dict is None:
|
||||
self.create_capabilities_dict()
|
||||
return self.capability_dict
|
||||
|
||||
|
||||
def get_capabilities_json(self) -> str:
|
||||
"""Get capabilities as formatted JSON string."""
|
||||
return json.dumps(self.get_capabilities_dict(), indent=2)
|
||||
|
||||
|
||||
def get_supported_actions(self) -> List[str]:
|
||||
"""Get list of all supported action names."""
|
||||
return list(self.action_registry.keys())
|
||||
@@ -234,81 +264,109 @@ class ServerCapabilities:
|
||||
|
||||
# Sample Action Implementations for demonstration
|
||||
|
||||
|
||||
class PingAction(AlpineBitsAction):
|
||||
"""Implementation for OTA_Ping action (handshaking)."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, config: Dict = None):
|
||||
self.name = AlpineBitsActionName.OTA_PING
|
||||
self.version = [Version.V2024_10, Version.V2022_10] # Supports multiple versions
|
||||
|
||||
async def handle(self, action: str, request_xml: str, version: Version, server_capabilities: None | ServerCapabilities = None) -> AlpineBitsResponse:
|
||||
self.version = [
|
||||
Version.V2024_10,
|
||||
Version.V2022_10,
|
||||
] # Supports multiple versions
|
||||
self.config = config
|
||||
|
||||
async def handle(
|
||||
self,
|
||||
action: str,
|
||||
request_xml: str,
|
||||
version: Version,
|
||||
server_capabilities: None | ServerCapabilities = None,
|
||||
) -> AlpineBitsResponse:
|
||||
"""Handle ping requests."""
|
||||
|
||||
if request_xml is None:
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Xml Request missing", HttpStatusCode.BAD_REQUEST
|
||||
)
|
||||
|
||||
if server_capabilities is None:
|
||||
return AlpineBitsResponse("Error: Something went wrong", HttpStatusCode.INTERNAL_SERVER_ERROR)
|
||||
|
||||
return AlpineBitsResponse(
|
||||
"Error: Something went wrong", HttpStatusCode.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# Parse the incoming request XML and extract EchoData
|
||||
parser = XmlParser()
|
||||
|
||||
try:
|
||||
parsed_request = parser.from_string(request_xml, OtaPingRq)
|
||||
|
||||
echo_data = json.loads(parsed_request.echo_data)
|
||||
except Exception as e:
|
||||
return AlpineBitsResponse(f"Error: Invalid XML request - {str(e)}", HttpStatusCode.BAD_REQUEST)
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Invalid XML request", HttpStatusCode.BAD_REQUEST
|
||||
)
|
||||
|
||||
# compare echo data with capabilities, create a dictionary containing the matching capabilities
|
||||
capabilities_dict = server_capabilities.get_capabilities_dict()
|
||||
|
||||
_LOGGER.info(f"Capabilities Dict: {capabilities_dict}")
|
||||
matching_capabilities = {"versions": []}
|
||||
|
||||
|
||||
# Iterate through client's requested versions
|
||||
for client_version in echo_data.get("versions", []):
|
||||
client_version_str = client_version.get("version", "")
|
||||
|
||||
|
||||
# Find matching server version
|
||||
for server_version in capabilities_dict["versions"]:
|
||||
if server_version["version"] == client_version_str:
|
||||
# Found a matching version, now find common actions
|
||||
matching_version = {
|
||||
"version": client_version_str,
|
||||
"actions": []
|
||||
}
|
||||
|
||||
matching_version = {"version": client_version_str, "actions": []}
|
||||
|
||||
# Get client's requested actions for this version
|
||||
client_actions = {action.get("action", ""): action for action in client_version.get("actions", [])}
|
||||
server_actions = {action.get("action", ""): action for action in server_version.get("actions", [])}
|
||||
|
||||
client_actions = {
|
||||
action.get("action", ""): action
|
||||
for action in client_version.get("actions", [])
|
||||
}
|
||||
server_actions = {
|
||||
action.get("action", ""): action
|
||||
for action in server_version.get("actions", [])
|
||||
}
|
||||
|
||||
# Find common actions
|
||||
for action_name in client_actions:
|
||||
if action_name in server_actions:
|
||||
# Use server's action definition (includes our supports)
|
||||
matching_version["actions"].append(server_actions[action_name])
|
||||
|
||||
matching_version["actions"].append(
|
||||
server_actions[action_name]
|
||||
)
|
||||
|
||||
# Only add version if there are common actions
|
||||
if matching_version["actions"]:
|
||||
matching_capabilities["versions"].append(matching_version)
|
||||
break
|
||||
|
||||
|
||||
# Debug print to see what we matched
|
||||
print("🔍 Client requested capabilities:")
|
||||
print(json.dumps(echo_data, indent=2))
|
||||
print("\n🏠 Server capabilities:")
|
||||
print(json.dumps(capabilities_dict, indent=2))
|
||||
print("\n🤝 Matching capabilities:")
|
||||
print(json.dumps(matching_capabilities, indent=2))
|
||||
|
||||
|
||||
# Create successful ping response with matched capabilities
|
||||
capabilities_json = json.dumps(matching_capabilities, indent=2)
|
||||
|
||||
warning = OtaPingRs.Warnings.Warning(status=WarningStatus.ALPINEBITS_HANDSHAKE.value, type_value="11", content=[capabilities_json])
|
||||
warning = OtaPingRs.Warnings.Warning(
|
||||
status=WarningStatus.ALPINEBITS_HANDSHAKE.value,
|
||||
type_value="11",
|
||||
content=[capabilities_json],
|
||||
)
|
||||
|
||||
warning_response = OtaPingRs.Warnings(warning=[warning])
|
||||
|
||||
response_ota_ping = OtaPingRs(version= "7.000", warnings=warning_response, echo_data=capabilities_json, success="")
|
||||
|
||||
|
||||
|
||||
all_capabilities = server_capabilities.get_capabilities_json()
|
||||
|
||||
response_ota_ping = OtaPingRs(
|
||||
version="7.000",
|
||||
warnings=warning_response,
|
||||
echo_data=all_capabilities,
|
||||
success="",
|
||||
)
|
||||
|
||||
config = SerializerConfig(
|
||||
pretty_print=True, xml_declaration=True, encoding="UTF-8"
|
||||
@@ -316,103 +374,233 @@ class PingAction(AlpineBitsAction):
|
||||
|
||||
serializer = XmlSerializer(config=config)
|
||||
|
||||
response_xml = serializer.render(response_ota_ping, ns_map={None: "http://www.opentravel.org/OTA/2003/05"})
|
||||
|
||||
|
||||
|
||||
response_xml = serializer.render(
|
||||
response_ota_ping, ns_map={None: "http://www.opentravel.org/OTA/2003/05"}
|
||||
)
|
||||
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
def strip_control_chars(s):
|
||||
# Remove all control characters (ASCII < 32 and DEL)
|
||||
return re.sub(r'[\x00-\x1F\x7F]', '', s)
|
||||
|
||||
def validate_hotel_authentication(username: str, password: str, hotelid: str, config: Dict) -> bool:
|
||||
""" Validate hotel authentication based on username, password, and hotel ID.
|
||||
|
||||
Example config
|
||||
alpine_bits_auth:
|
||||
- hotel_id: "123"
|
||||
hotel_name: "Frangart Inn"
|
||||
username: "alice"
|
||||
password: !secret ALICE_PASSWORD
|
||||
"""
|
||||
|
||||
if not config or "alpine_bits_auth" not in config:
|
||||
return False
|
||||
auth_list = config["alpine_bits_auth"]
|
||||
for auth in auth_list:
|
||||
if (
|
||||
auth.get("hotel_id") == hotelid
|
||||
and auth.get("username") == username
|
||||
and auth.get("password") == password
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
# look for hotelid in config
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ReadAction(AlpineBitsAction):
|
||||
"""Implementation for OTA_Read action."""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, config: Dict = None):
|
||||
self.name = AlpineBitsActionName.OTA_READ
|
||||
self.version = [Version.V2024_10, Version.V2022_10]
|
||||
|
||||
async def handle(self, action: str, request_xml: str, version: Version) -> AlpineBitsResponse:
|
||||
self.config = config
|
||||
|
||||
async def handle(
|
||||
self, action: str, request_xml: str, version: Version, dbsession=None, username=None, password=None
|
||||
) -> AlpineBitsResponse:
|
||||
"""Handle read requests."""
|
||||
response_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OTA_ReadRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="8.000">
|
||||
<Success/>
|
||||
<Data>Read operation successful for {version.value}</Data>
|
||||
</OTA_ReadRS>'''
|
||||
|
||||
clean_action = strip_control_chars(str(action)).strip()
|
||||
clean_expected = strip_control_chars(self.name.value[1]).strip()
|
||||
|
||||
if clean_action != clean_expected:
|
||||
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Invalid action {action}, expected {self.name.value[1]}", HttpStatusCode.BAD_REQUEST
|
||||
)
|
||||
|
||||
if dbsession is None:
|
||||
return AlpineBitsResponse(
|
||||
"Error: Something went wrong", HttpStatusCode.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
read_request = XmlParser().from_string(request_xml, OtaReadRq)
|
||||
|
||||
hotel_read_request = read_request.read_requests.hotel_read_request
|
||||
|
||||
hotelid = hotel_read_request.hotel_code
|
||||
hotelname = hotel_read_request.hotel_name
|
||||
|
||||
if hotelname is None:
|
||||
hotelname = "unknown"
|
||||
|
||||
if username is None or password is None or hotelid is None:
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Unauthorized Read Request for this specific hotel {hotelname}. Check credentials", HttpStatusCode.UNAUTHORIZED
|
||||
)
|
||||
|
||||
if not validate_hotel_authentication(username, password, hotelid, self.config):
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Unauthorized Read Request for this specific hotel {hotelname}. Check credentials", HttpStatusCode.UNAUTHORIZED
|
||||
)
|
||||
|
||||
start_date = None
|
||||
|
||||
if hotel_read_request.selection_criteria is not None:
|
||||
start_date = datetime.fromisoformat(hotel_read_request.selection_criteria.start)
|
||||
|
||||
|
||||
|
||||
# query all reservations for this hotel from the database, where start_date is greater than or equal to the given start_date
|
||||
|
||||
stmt = (
|
||||
select(Reservation, Customer)
|
||||
.join(Customer, Reservation.customer_id == Customer.id)
|
||||
.filter(Reservation.hotel_code == hotelid)
|
||||
)
|
||||
if start_date:
|
||||
stmt = stmt.filter(Reservation.start_date >= start_date)
|
||||
|
||||
result = await dbsession.execute(stmt)
|
||||
reservation_customer_pairs: list[tuple[Reservation, Customer]] = result.all() # List of (Reservation, Customer) tuples
|
||||
|
||||
_LOGGER.info(f"Querying reservations and customers for hotel {hotelid} from database")
|
||||
for reservation, customer in reservation_customer_pairs:
|
||||
_LOGGER.info(f"Reservation: {reservation.id}, Customer: {customer.given_name}")
|
||||
|
||||
res_retrive_rs = create_xml_from_db(reservation_customer_pairs)
|
||||
|
||||
config = SerializerConfig(
|
||||
pretty_print=True, xml_declaration=True, encoding="UTF-8"
|
||||
)
|
||||
serializer = XmlSerializer(config=config)
|
||||
response_xml = serializer.render(
|
||||
res_retrive_rs, ns_map={None: "http://www.opentravel.org/OTA/2003/05"}
|
||||
)
|
||||
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# For demonstration, just echo back a simple XML response
|
||||
response_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OTA_ReadRS xmlns="http://www.opentravel.org/OTA/2003/
|
||||
05" Version="8.000">
|
||||
<Success/>
|
||||
</OTA_ReadRS>"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
|
||||
|
||||
class HotelAvailNotifAction(AlpineBitsAction):
|
||||
"""Implementation for Hotel Availability Notification action with supports."""
|
||||
|
||||
def __init__(self):
|
||||
self.name = AlpineBitsActionName.OTA_HOTEL_AVAIL_NOTIF
|
||||
self.version = Version.V2022_10
|
||||
self.supports = [
|
||||
"OTA_HotelAvailNotif_accept_rooms",
|
||||
"OTA_HotelAvailNotif_accept_categories",
|
||||
"OTA_HotelAvailNotif_accept_deltas",
|
||||
"OTA_HotelAvailNotif_accept_BookingThreshold"
|
||||
]
|
||||
|
||||
async def handle(self, action: str, request_xml: str, version: Version) -> AlpineBitsResponse:
|
||||
"""Handle hotel availability notifications."""
|
||||
response_xml = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<OTA_HotelAvailNotifRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="8.000">
|
||||
<Success/>
|
||||
</OTA_HotelAvailNotifRS>'''
|
||||
return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
# class HotelAvailNotifAction(AlpineBitsAction):
|
||||
# """Implementation for Hotel Availability Notification action with supports."""
|
||||
|
||||
# def __init__(self):
|
||||
# self.name = AlpineBitsActionName.OTA_HOTEL_AVAIL_NOTIF
|
||||
# self.version = Version.V2022_10
|
||||
# self.supports = [
|
||||
# "OTA_HotelAvailNotif_accept_rooms",
|
||||
# "OTA_HotelAvailNotif_accept_categories",
|
||||
# "OTA_HotelAvailNotif_accept_deltas",
|
||||
# "OTA_HotelAvailNotif_accept_BookingThreshold",
|
||||
# ]
|
||||
|
||||
# async def handle(
|
||||
# self, action: str, request_xml: str, version: Version
|
||||
# ) -> AlpineBitsResponse:
|
||||
# """Handle hotel availability notifications."""
|
||||
# response_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
# <OTA_HotelAvailNotifRS xmlns="http://www.opentravel.org/OTA/2003/05" Version="8.000">
|
||||
# <Success/>
|
||||
# </OTA_HotelAvailNotifRS>"""
|
||||
# return AlpineBitsResponse(response_xml, HttpStatusCode.OK)
|
||||
|
||||
|
||||
class GuestRequestsAction(AlpineBitsAction):
|
||||
"""Unimplemented action - will not appear in capabilities."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.name = AlpineBitsActionName.OTA_HOTEL_RES_NOTIF_GUEST_REQUESTS
|
||||
self.version = Version.V2024_10
|
||||
|
||||
|
||||
# Note: This class doesn't override the handle method, so it won't be discovered
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class AlpineBitsServer:
|
||||
"""
|
||||
Asynchronous AlpineBits server for handling hotel data exchange requests.
|
||||
|
||||
|
||||
This server handles various OTA actions and implements the AlpineBits protocol
|
||||
for hotel data exchange. It maintains a registry of supported actions and
|
||||
their capabilities, and can respond to handshake requests with its capabilities.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
||||
def __init__(self, config: Dict = None):
|
||||
self.capabilities = ServerCapabilities()
|
||||
self._action_instances = {}
|
||||
self.config = config
|
||||
self._initialize_action_instances()
|
||||
|
||||
|
||||
|
||||
def _initialize_action_instances(self):
|
||||
"""Initialize instances of all discovered action classes."""
|
||||
for capability_name, action_class in self.capabilities.action_registry.items():
|
||||
self._action_instances[capability_name] = action_class()
|
||||
|
||||
self._action_instances[capability_name] = action_class(config=self.config)
|
||||
|
||||
def get_capabilities(self) -> Dict:
|
||||
"""Get server capabilities."""
|
||||
return self.capabilities.get_capabilities_dict()
|
||||
|
||||
|
||||
def get_capabilities_json(self) -> str:
|
||||
"""Get server capabilities as JSON."""
|
||||
return self.capabilities.get_capabilities_json()
|
||||
|
||||
async def handle_request(self, request_action_name: str, request_xml: str, version: str = "2024-10") -> AlpineBitsResponse:
|
||||
|
||||
async def handle_request(
|
||||
self, request_action_name: str, request_xml: str, version: str = "2024-10", dbsession=None, username=None, password=None
|
||||
) -> AlpineBitsResponse:
|
||||
"""
|
||||
Handle an incoming AlpineBits request by routing to appropriate action handler.
|
||||
|
||||
|
||||
Args:
|
||||
request_action_name: The action name from the request (e.g., "OTA_Read:GuestRequests")
|
||||
request_xml: The XML request body
|
||||
version: The AlpineBits version (defaults to "2024-10")
|
||||
|
||||
|
||||
Returns:
|
||||
AlpineBitsResponse with the result
|
||||
"""
|
||||
@@ -421,52 +609,56 @@ class AlpineBitsServer:
|
||||
version_enum = Version(version)
|
||||
except ValueError:
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Unsupported version {version}",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
f"Error: Unsupported version {version}", HttpStatusCode.BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Find the action by request name
|
||||
action_enum = AlpineBitsActionName.get_by_request_name(request_action_name)
|
||||
if not action_enum:
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Unknown action {request_action_name}",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
HttpStatusCode.BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
# Check if we have an implementation for this action
|
||||
capability_name = action_enum.capability_name
|
||||
if capability_name not in self._action_instances:
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Action {request_action_name} is not implemented",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
HttpStatusCode.BAD_REQUEST,
|
||||
)
|
||||
|
||||
action_instance = self._action_instances[capability_name]
|
||||
|
||||
|
||||
action_instance: AlpineBitsAction = self._action_instances[capability_name]
|
||||
|
||||
# Check if the action supports the requested version
|
||||
if not await action_instance.check_version_supported(version_enum):
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Action {request_action_name} does not support version {version}",
|
||||
HttpStatusCode.BAD_REQUEST
|
||||
HttpStatusCode.BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
# Handle the request
|
||||
try:
|
||||
# Special case for ping action - pass server capabilities
|
||||
if capability_name == "action_OTA_Ping":
|
||||
return await action_instance.handle(request_action_name, request_xml, version_enum, self.capabilities)
|
||||
return await action_instance.handle(
|
||||
request_action_name, request_xml, version_enum, self.capabilities
|
||||
)
|
||||
else:
|
||||
return await action_instance.handle(request_action_name, request_xml, version_enum)
|
||||
return await action_instance.handle(
|
||||
request_action_name, request_xml, version_enum, dbsession=dbsession, username=username, password=password
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error handling request {request_action_name}: {str(e)}")
|
||||
# print stack trace for debugging
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return AlpineBitsResponse(
|
||||
f"Error: Internal server error while processing {request_action_name}: {str(e)}",
|
||||
HttpStatusCode.INTERNAL_SERVER_ERROR
|
||||
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
def get_supported_request_names(self) -> List[str]:
|
||||
"""Get all supported request names (not capability names)."""
|
||||
request_names = []
|
||||
@@ -475,26 +667,28 @@ class AlpineBitsServer:
|
||||
if action_enum:
|
||||
request_names.append(action_enum.request_name)
|
||||
return sorted(request_names)
|
||||
|
||||
def is_action_supported(self, request_action_name: str, version: str = None) -> bool:
|
||||
|
||||
def is_action_supported(
|
||||
self, request_action_name: str, version: str = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if a request action is supported.
|
||||
|
||||
|
||||
Args:
|
||||
request_action_name: The request action name (e.g., "OTA_Read:GuestRequests")
|
||||
version: Optional version to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if supported, False otherwise
|
||||
"""
|
||||
action_enum = AlpineBitsActionName.get_by_request_name(request_action_name)
|
||||
if not action_enum:
|
||||
return False
|
||||
|
||||
|
||||
capability_name = action_enum.capability_name
|
||||
if capability_name not in self._action_instances:
|
||||
return False
|
||||
|
||||
|
||||
if version:
|
||||
try:
|
||||
version_enum = Version(version)
|
||||
@@ -506,7 +700,7 @@ class AlpineBitsServer:
|
||||
return action_instance.version == version_enum
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -514,10 +708,10 @@ async def main():
|
||||
"""Demonstrate the automatic capabilities discovery and request handling."""
|
||||
print("🚀 AlpineBits Server Capabilities Discovery & Request Handling Demo")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
# Create server instance
|
||||
server = AlpineBitsServer()
|
||||
|
||||
|
||||
print("\n📋 Discovered Action Classes:")
|
||||
print("-" * 30)
|
||||
for capability_name, action_class in server.capabilities.action_registry.items():
|
||||
@@ -525,24 +719,26 @@ async def main():
|
||||
request_name = action_enum.request_name if action_enum else "unknown"
|
||||
print(f"✅ {capability_name} -> {action_class.__name__}")
|
||||
print(f" Request name: {request_name}")
|
||||
|
||||
print(f"\n📊 Total Implemented Actions: {len(server.capabilities.get_supported_actions())}")
|
||||
|
||||
|
||||
print(
|
||||
f"\n📊 Total Implemented Actions: {len(server.capabilities.get_supported_actions())}"
|
||||
)
|
||||
|
||||
print("\n🔍 Generated Capabilities JSON:")
|
||||
print("-" * 30)
|
||||
capabilities_json = server.get_capabilities_json()
|
||||
print(capabilities_json)
|
||||
|
||||
|
||||
print("\n🎯 Supported Request Names:")
|
||||
print("-" * 30)
|
||||
for request_name in server.get_supported_request_names():
|
||||
print(f" • {request_name}")
|
||||
|
||||
|
||||
print("\n🧪 Testing Request Handling:")
|
||||
print("-" * 30)
|
||||
|
||||
|
||||
test_xml = "<test>sample request</test>"
|
||||
|
||||
|
||||
# Test different request formats
|
||||
test_cases = [
|
||||
("OTA_Ping:Handshaking", "2024-10"),
|
||||
@@ -550,16 +746,16 @@ async def main():
|
||||
("OTA_Read:GuestRequests", "2022-10"),
|
||||
("OTA_HotelAvailNotif", "2024-10"),
|
||||
("UnknownAction", "2024-10"),
|
||||
("OTA_Ping:Handshaking", "unsupported-version")
|
||||
("OTA_Ping:Handshaking", "unsupported-version"),
|
||||
]
|
||||
|
||||
|
||||
for request_name, version in test_cases:
|
||||
print(f"\n<EFBFBD> Testing: {request_name} (v{version})")
|
||||
|
||||
|
||||
# Check if supported first
|
||||
is_supported = server.is_action_supported(request_name, version)
|
||||
print(f" Supported: {is_supported}")
|
||||
|
||||
|
||||
# Handle the request
|
||||
response = await server.handle_request(request_name, test_xml, version)
|
||||
print(f" Status: {response.status_code}")
|
||||
@@ -567,9 +763,9 @@ async def main():
|
||||
print(f" Response: {response.xml_content[:100]}...")
|
||||
else:
|
||||
print(f" Response: {response.xml_content}")
|
||||
|
||||
|
||||
print("\n✅ Demo completed successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(main())
|
||||
|
||||
716
src/alpine_bits_python/api.py
Normal file
716
src/alpine_bits_python/api.py
Normal file
@@ -0,0 +1,716 @@
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
HTTPException,
|
||||
BackgroundTasks,
|
||||
Request,
|
||||
Depends,
|
||||
APIRouter,
|
||||
Form,
|
||||
File,
|
||||
UploadFile,
|
||||
)
|
||||
from fastapi.concurrency import asynccontextmanager
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import HTTPBearer, HTTPBasicCredentials, HTTPBasic
|
||||
from .config_loader import load_config
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse, Response
|
||||
from .models import WixFormSubmission
|
||||
from datetime import datetime, date, timezone
|
||||
from .auth import validate_api_key, validate_wix_signature, generate_api_key
|
||||
from .rate_limit import (
|
||||
limiter,
|
||||
webhook_limiter,
|
||||
custom_rate_limit_handler,
|
||||
DEFAULT_RATE_LIMIT,
|
||||
WEBHOOK_RATE_LIMIT,
|
||||
BURST_RATE_LIMIT,
|
||||
)
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, List
|
||||
import json
|
||||
import os
|
||||
import gzip
|
||||
import xml.etree.ElementTree as ET
|
||||
from .alpinebits_server import AlpineBitsServer, Version
|
||||
import urllib.parse
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
|
||||
from .db import (
|
||||
Base,
|
||||
Customer as DBCustomer,
|
||||
Reservation as DBReservation,
|
||||
get_database_url,
|
||||
)
|
||||
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# HTTP Basic auth for AlpineBits
|
||||
security_basic = HTTPBasic()
|
||||
|
||||
# Load config at startup
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Setup DB
|
||||
|
||||
try:
|
||||
config = load_config()
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Failed to load config: {str(e)}")
|
||||
config = {}
|
||||
|
||||
DATABASE_URL = get_database_url(config)
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
app.state.engine = engine
|
||||
app.state.async_sessionmaker = AsyncSessionLocal
|
||||
app.state.config = config
|
||||
app.state.alpine_bits_server = AlpineBitsServer(config)
|
||||
|
||||
# Create tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
_LOGGER.info("Database tables checked/created at startup.")
|
||||
|
||||
yield
|
||||
|
||||
# Optional: Dispose engine on shutdown
|
||||
await engine.dispose()
|
||||
|
||||
async def get_async_session(request: Request):
|
||||
async_sessionmaker = request.app.state.async_sessionmaker
|
||||
async with async_sessionmaker() as session:
|
||||
yield session
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Wix Form Handler API",
|
||||
description="Secure API endpoint to receive and process Wix form submissions with authentication and rate limiting",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# Create API router with /api prefix
|
||||
api_router = APIRouter(prefix="/api", tags=["api"])
|
||||
|
||||
# Add rate limiting
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, custom_rate_limit_handler)
|
||||
|
||||
# Add CORS middleware to allow requests from Wix
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"https://*.wix.com",
|
||||
"https://*.wixstatic.com",
|
||||
"http://localhost:3000", # For development
|
||||
"http://localhost:8000", # For local testing
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
async def process_form_submission(submission_data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Background task to process the form submission.
|
||||
Add your business logic here.
|
||||
"""
|
||||
try:
|
||||
_LOGGER.info(
|
||||
f"Processing form submission: {submission_data.get('submissionId')}"
|
||||
)
|
||||
|
||||
# Example processing - you can replace this with your actual logic
|
||||
form_name = submission_data.get("formName")
|
||||
contact_email = (
|
||||
submission_data.get("contact", {}).get("email")
|
||||
if submission_data.get("contact")
|
||||
else None
|
||||
)
|
||||
|
||||
# Extract form fields
|
||||
form_fields = {
|
||||
k: v for k, v in submission_data.items() if k.startswith("field:")
|
||||
}
|
||||
|
||||
_LOGGER.info(
|
||||
f"Form: {form_name}, Contact: {contact_email}, Fields: {len(form_fields)}"
|
||||
)
|
||||
|
||||
# Here you could:
|
||||
# - Save to database
|
||||
# - Send emails
|
||||
# - Call external APIs
|
||||
# - Process the data further
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error processing form submission: {str(e)}")
|
||||
|
||||
|
||||
|
||||
|
||||
@api_router.get("/")
|
||||
@limiter.limit(DEFAULT_RATE_LIMIT)
|
||||
async def root(request: Request):
|
||||
"""Health check endpoint"""
|
||||
return {
|
||||
"message": "Wix Form Handler API is running",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"status": "healthy",
|
||||
"authentication": "required",
|
||||
"rate_limits": {
|
||||
"default": DEFAULT_RATE_LIMIT,
|
||||
"webhook": WEBHOOK_RATE_LIMIT,
|
||||
"burst": BURST_RATE_LIMIT,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@api_router.get("/health")
|
||||
@limiter.limit(DEFAULT_RATE_LIMIT)
|
||||
async def health_check(request: Request):
|
||||
"""Detailed health check"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"service": "wix-form-handler",
|
||||
"version": "1.0.0",
|
||||
"authentication": "enabled",
|
||||
"rate_limiting": "enabled",
|
||||
}
|
||||
|
||||
|
||||
# Extracted business logic for handling Wix form submissions
|
||||
async def process_wix_form_submission(request: Request, data: Dict[str, Any], db):
|
||||
"""
|
||||
Shared business logic for handling Wix form submissions (test and production).
|
||||
"""
|
||||
timestamp = datetime.now().isoformat()
|
||||
|
||||
_LOGGER.info(f"Received Wix form data at {timestamp}")
|
||||
# _LOGGER.info(f"Data keys: {list(data.keys())}")
|
||||
# _LOGGER.info(f"Full data: {json.dumps(data, indent=2)}")
|
||||
log_entry = {
|
||||
"timestamp": timestamp,
|
||||
"client_ip": request.client.host if request.client else "unknown",
|
||||
"headers": dict(request.headers),
|
||||
"data": data,
|
||||
"origin_header": request.headers.get("origin"),
|
||||
"all_headers": dict(request.headers),
|
||||
}
|
||||
logs_dir = "logs"
|
||||
if not os.path.exists(logs_dir):
|
||||
os.makedirs(logs_dir, mode=0o755, exist_ok=True)
|
||||
stat_info = os.stat(logs_dir)
|
||||
_LOGGER.info(
|
||||
f"Created directory owner: uid:{stat_info.st_uid}, gid:{stat_info.st_gid}"
|
||||
)
|
||||
_LOGGER.info(f"Directory mode: {oct(stat_info.st_mode)[-3:]}")
|
||||
log_filename = (
|
||||
f"{logs_dir}/wix_test_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
)
|
||||
with open(log_filename, "w", encoding="utf-8") as f:
|
||||
json.dump(log_entry, f, indent=2, default=str, ensure_ascii=False)
|
||||
file_stat = os.stat(log_filename)
|
||||
_LOGGER.info(f"Created file owner: uid:{file_stat.st_uid}, gid:{file_stat.st_gid}")
|
||||
_LOGGER.info(f"File mode: {oct(file_stat.st_mode)[-3:]}")
|
||||
_LOGGER.info(f"Data logged to: {log_filename}")
|
||||
|
||||
data = data.get("data") # Handle nested "data" key if present
|
||||
|
||||
# save customer and reservation to DB
|
||||
|
||||
contact_info = data.get("contact", {})
|
||||
first_name = contact_info.get("name", {}).get("first")
|
||||
last_name = contact_info.get("name", {}).get("last")
|
||||
email = contact_info.get("email")
|
||||
phone_number = contact_info.get("phones", [{}])[0].get("e164Phone")
|
||||
locale = contact_info.get("locale", "de-de")
|
||||
contact_id = contact_info.get("contactId")
|
||||
|
||||
name_prefix = data.get("field:anrede")
|
||||
email_newsletter = data.get("field:form_field_5a7b", "") != "Non selezionato"
|
||||
address_line = None
|
||||
city_name = None
|
||||
postal_code = None
|
||||
country_code = None
|
||||
gender = None
|
||||
birth_date = None
|
||||
language = data.get("contact", {}).get("locale", "en")[:2]
|
||||
|
||||
# Dates
|
||||
start_date = (
|
||||
data.get("field:date_picker_a7c8")
|
||||
or data.get("Anreisedatum")
|
||||
or data.get("submissions", [{}])[1].get("value")
|
||||
)
|
||||
end_date = (
|
||||
data.get("field:date_picker_7e65")
|
||||
or data.get("Abreisedatum")
|
||||
or data.get("submissions", [{}])[2].get("value")
|
||||
)
|
||||
|
||||
# Room/guest info
|
||||
num_adults = int(data.get("field:number_7cf5") or 2)
|
||||
num_children = int(data.get("field:anzahl_kinder") or 0)
|
||||
children_ages = []
|
||||
if num_children > 0:
|
||||
for k in data.keys():
|
||||
if k.startswith("field:alter_kind_"):
|
||||
try:
|
||||
age = int(data[k])
|
||||
children_ages.append(age)
|
||||
except ValueError:
|
||||
_LOGGER.warning(f"Invalid age value for {k}: {data[k]}")
|
||||
|
||||
offer = data.get("field:angebot_auswaehlen")
|
||||
|
||||
# UTM and offer
|
||||
utm_fields = [
|
||||
("utm_Source", "utm_source"),
|
||||
("utm_Medium", "utm_medium"),
|
||||
("utm_Campaign", "utm_campaign"),
|
||||
("utm_Term", "utm_term"),
|
||||
("utm_Content", "utm_content"),
|
||||
]
|
||||
utm_comment_text = []
|
||||
for label, field in utm_fields:
|
||||
val = data.get(f"field:{field}") or data.get(label)
|
||||
if val:
|
||||
utm_comment_text.append(f"{label}: {val}")
|
||||
utm_comment = ",".join(utm_comment_text) if utm_comment_text else None
|
||||
|
||||
# use database session
|
||||
|
||||
# Save all relevant data to DB (including new fields)
|
||||
db_customer = DBCustomer(
|
||||
given_name=first_name,
|
||||
surname=last_name,
|
||||
contact_id=contact_id,
|
||||
name_prefix=name_prefix,
|
||||
email_address=email,
|
||||
phone=phone_number,
|
||||
email_newsletter=email_newsletter,
|
||||
address_line=address_line,
|
||||
city_name=city_name,
|
||||
postal_code=postal_code,
|
||||
country_code=country_code,
|
||||
gender=gender,
|
||||
birth_date=birth_date,
|
||||
language=language,
|
||||
address_catalog=False,
|
||||
name_title=None,
|
||||
)
|
||||
db.add(db_customer)
|
||||
await db.commit()
|
||||
await db.refresh(db_customer)
|
||||
|
||||
db_reservation = DBReservation(
|
||||
customer_id=db_customer.id,
|
||||
form_id=data.get("submissionId"),
|
||||
start_date=date.fromisoformat(start_date) if start_date else None,
|
||||
end_date=date.fromisoformat(end_date) if end_date else None,
|
||||
num_adults=num_adults,
|
||||
num_children=num_children,
|
||||
children_ages=",".join(str(a) for a in children_ages),
|
||||
offer=offer,
|
||||
utm_comment=utm_comment,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
utm_source=data.get("field:utm_source"),
|
||||
utm_medium=data.get("field:utm_medium"),
|
||||
utm_campaign=data.get("field:utm_campaign"),
|
||||
utm_term=data.get("field:utm_term"),
|
||||
utm_content=data.get("field:utm_content"),
|
||||
user_comment=data.get("field:long_answer_3524", ""),
|
||||
fbclid=data.get("field:fbclid"),
|
||||
gclid=data.get("field:gclid"),
|
||||
hotel_code="123",
|
||||
hotel_name="Frangart Inn",
|
||||
)
|
||||
db.add(db_reservation)
|
||||
await db.commit()
|
||||
await db.refresh(db_reservation)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Wix form data received successfully",
|
||||
"received_keys": list(data.keys()),
|
||||
"data_logged_to": log_filename,
|
||||
"timestamp": timestamp,
|
||||
"note": "No authentication required for this endpoint",
|
||||
}
|
||||
|
||||
|
||||
@api_router.post("/webhook/wix-form")
|
||||
@webhook_limiter.limit(WEBHOOK_RATE_LIMIT)
|
||||
async def handle_wix_form(
|
||||
request: Request, data: Dict[str, Any], db_session=Depends(get_async_session)
|
||||
):
|
||||
"""
|
||||
Unified endpoint to handle Wix form submissions (test and production).
|
||||
No authentication required for this endpoint.
|
||||
"""
|
||||
try:
|
||||
return await process_wix_form_submission(request, data, db_session)
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error in handle_wix_form: {str(e)}")
|
||||
# log stacktrace
|
||||
import traceback
|
||||
|
||||
traceback_str = traceback.format_exc()
|
||||
_LOGGER.error(f"Stack trace for handle_wix_form: {traceback_str}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error processing Wix form data: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@api_router.post("/webhook/wix-form/test")
|
||||
@limiter.limit(DEFAULT_RATE_LIMIT)
|
||||
async def handle_wix_form_test(
|
||||
request: Request, data: Dict[str, Any], db_session=Depends(get_async_session)
|
||||
):
|
||||
"""
|
||||
Test endpoint to verify the API is working with raw JSON data.
|
||||
No authentication required for testing purposes.
|
||||
"""
|
||||
try:
|
||||
return await process_wix_form_submission(request, data, db_session)
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error in handle_wix_form_test: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error processing test data: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@api_router.post("/admin/generate-api-key")
|
||||
@limiter.limit("5/hour") # Very restrictive for admin operations
|
||||
async def generate_new_api_key(
|
||||
request: Request, admin_key: str = Depends(validate_api_key)
|
||||
):
|
||||
"""
|
||||
Admin endpoint to generate new API keys.
|
||||
Requires admin API key and is heavily rate limited.
|
||||
"""
|
||||
if admin_key != "admin-key":
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
new_key = generate_api_key()
|
||||
_LOGGER.info(f"Generated new API key (requested by: {admin_key})")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "New API key generated",
|
||||
"api_key": new_key,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"note": "Store this key securely - it won't be shown again",
|
||||
}
|
||||
|
||||
|
||||
async def validate_basic_auth(
|
||||
credentials: HTTPBasicCredentials = Depends(security_basic),
|
||||
) -> str:
|
||||
"""
|
||||
Validate basic authentication for AlpineBits protocol.
|
||||
Returns username if valid, raises HTTPException if not.
|
||||
"""
|
||||
# Accept any username/password pair present in config['alpine_bits_auth']
|
||||
if not credentials.username or not credentials.password:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="ERROR: Authentication required",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
valid = False
|
||||
config = app.state.config
|
||||
|
||||
for entry in config["alpine_bits_auth"]:
|
||||
if (
|
||||
credentials.username == entry["username"]
|
||||
and credentials.password == entry["password"]
|
||||
):
|
||||
valid = True
|
||||
break
|
||||
if not valid:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="ERROR: Invalid credentials",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
_LOGGER.info(
|
||||
f"AlpineBits authentication successful for user: {credentials.username} (from config)"
|
||||
)
|
||||
return credentials.username, credentials.password
|
||||
|
||||
|
||||
def parse_multipart_data(content_type: str, body: bytes) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse multipart/form-data from raw request body.
|
||||
This is a simplified parser for the AlpineBits use case.
|
||||
"""
|
||||
if "multipart/form-data" not in content_type:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="ERROR: Content-Type must be multipart/form-data"
|
||||
)
|
||||
|
||||
# Extract boundary
|
||||
boundary = None
|
||||
for part in content_type.split(";"):
|
||||
part = part.strip()
|
||||
if part.startswith("boundary="):
|
||||
boundary = part.split("=", 1)[1].strip('"')
|
||||
break
|
||||
|
||||
if not boundary:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="ERROR: Missing boundary in multipart/form-data"
|
||||
)
|
||||
|
||||
# Simple multipart parsing
|
||||
parts = body.split(f"--{boundary}".encode())
|
||||
data = {}
|
||||
|
||||
for part in parts:
|
||||
if not part.strip() or part.strip() == b"--":
|
||||
continue
|
||||
|
||||
# Split headers and content
|
||||
if b"\r\n\r\n" in part:
|
||||
headers_section, content = part.split(b"\r\n\r\n", 1)
|
||||
content = content.rstrip(b"\r\n")
|
||||
|
||||
# Parse Content-Disposition header
|
||||
headers = headers_section.decode("utf-8", errors="ignore")
|
||||
name = None
|
||||
for line in headers.split("\n"):
|
||||
if "Content-Disposition" in line and "name=" in line:
|
||||
# Extract name parameter
|
||||
for param in line.split(";"):
|
||||
param = param.strip()
|
||||
if param.startswith("name="):
|
||||
name = param.split("=", 1)[1].strip('"')
|
||||
break
|
||||
|
||||
if name:
|
||||
# Handle file uploads or text content
|
||||
if content.startswith(b"<"):
|
||||
# Likely XML content
|
||||
data[name] = content.decode("utf-8", errors="ignore")
|
||||
else:
|
||||
data[name] = content.decode("utf-8", errors="ignore")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@api_router.post("/alpinebits/server-2024-10")
|
||||
@limiter.limit("60/minute")
|
||||
async def alpinebits_server_handshake(
|
||||
request: Request, credentials_tupel: tuple = Depends(validate_basic_auth), dbsession=Depends(get_async_session)
|
||||
):
|
||||
"""
|
||||
AlpineBits server endpoint implementing the handshake protocol.
|
||||
|
||||
This endpoint handles:
|
||||
- Protocol version negotiation via X-AlpineBits-ClientProtocolVersion header
|
||||
- Client identification via X-AlpineBits-ClientID header (optional)
|
||||
- Multipart/form-data parsing for action and request parameters
|
||||
- Gzip compression support
|
||||
- Proper error handling with HTTP status codes
|
||||
- Handshaking action processing
|
||||
|
||||
Authentication: HTTP Basic Auth required
|
||||
Content-Type: multipart/form-data
|
||||
Compression: gzip supported (check X-AlpineBits-Server-Accept-Encoding)
|
||||
"""
|
||||
try:
|
||||
# Check required headers
|
||||
client_protocol_version = request.headers.get(
|
||||
"X-AlpineBits-ClientProtocolVersion"
|
||||
)
|
||||
if not client_protocol_version:
|
||||
# Server concludes client speaks a protocol version preceding 2013-04
|
||||
client_protocol_version = "pre-2013-04"
|
||||
_LOGGER.info(
|
||||
"No X-AlpineBits-ClientProtocolVersion header found, assuming pre-2013-04"
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(f"Client protocol version: {client_protocol_version}")
|
||||
|
||||
# Optional client ID
|
||||
client_id = request.headers.get("X-AlpineBits-ClientID")
|
||||
if client_id:
|
||||
_LOGGER.info(f"Client ID: {client_id}")
|
||||
|
||||
# Check content encoding
|
||||
content_encoding = request.headers.get("Content-Encoding")
|
||||
is_compressed = content_encoding == "gzip"
|
||||
|
||||
if is_compressed:
|
||||
_LOGGER.info("Request is gzip compressed")
|
||||
|
||||
# Get content type before processing
|
||||
content_type = request.headers.get("Content-Type", "")
|
||||
|
||||
_LOGGER.info(f"Content-Type: {content_type}")
|
||||
_LOGGER.info(f"Content-Encoding: {content_encoding}")
|
||||
|
||||
# Get request body
|
||||
body = await request.body()
|
||||
|
||||
# Decompress if needed
|
||||
if is_compressed:
|
||||
try:
|
||||
body = gzip.decompress(body)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"ERROR: Failed to decompress gzip content: {str(e)}",
|
||||
)
|
||||
|
||||
# Check content type (after decompression)
|
||||
if (
|
||||
"multipart/form-data" not in content_type
|
||||
and "application/x-www-form-urlencoded" not in content_type
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Content-Type must be multipart/form-data or application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
# Parse multipart data
|
||||
if "multipart/form-data" in content_type:
|
||||
try:
|
||||
form_data = parse_multipart_data(content_type, body)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"ERROR: Failed to parse multipart/form-data: {str(e)}",
|
||||
)
|
||||
elif "application/x-www-form-urlencoded" in content_type:
|
||||
# Parse as urlencoded
|
||||
form_data = dict(urllib.parse.parse_qsl(body.decode("utf-8")))
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="ERROR: Content-Type must be multipart/form-data or application/x-www-form-urlencoded",
|
||||
)
|
||||
|
||||
# Check for required action parameter
|
||||
action = form_data.get("action")
|
||||
if not action:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="ERROR: Missing required 'action' parameter"
|
||||
)
|
||||
|
||||
_LOGGER.info(f"AlpineBits action: {action}")
|
||||
|
||||
# Get optional request XML
|
||||
request_xml = form_data.get("request")
|
||||
|
||||
server = app.state.alpine_bits_server
|
||||
|
||||
version = Version.V2024_10
|
||||
|
||||
username, password = credentials_tupel
|
||||
|
||||
# Create successful handshake response
|
||||
response = await server.handle_request(action, request_xml, version, dbsession=dbsession, username=username, password=password)
|
||||
|
||||
response_xml = response.xml_content
|
||||
|
||||
# Set response headers indicating server capabilities
|
||||
headers = {
|
||||
"Content-Type": "application/xml; charset=utf-8",
|
||||
"X-AlpineBits-Server-Accept-Encoding": "gzip", # Indicate gzip support
|
||||
"X-AlpineBits-Server-Version": "2024-10",
|
||||
}
|
||||
|
||||
return Response(
|
||||
content=response_xml, status_code=response.status_code, headers=headers
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions (auth errors, etc.)
|
||||
raise
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error in AlpineBits handshake: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||
|
||||
|
||||
@api_router.get("/admin/stats")
|
||||
@limiter.limit("10/minute")
|
||||
async def get_api_stats(request: Request, admin_key: str = Depends(validate_api_key)):
|
||||
"""
|
||||
Admin endpoint to get API usage statistics.
|
||||
Requires admin API key.
|
||||
"""
|
||||
if admin_key != "admin-key":
|
||||
raise HTTPException(status_code=403, detail="Admin access required")
|
||||
|
||||
# In a real application, you'd fetch this from your database/monitoring system
|
||||
return {
|
||||
"status": "success",
|
||||
"stats": {
|
||||
"uptime": "Available in production deployment",
|
||||
"total_requests": "Available with monitoring setup",
|
||||
"active_api_keys": len([k for k in ["wix-webhook-key", "admin-key"] if k]),
|
||||
"rate_limit_backend": "redis" if os.getenv("REDIS_URL") else "memory",
|
||||
},
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# Include the API router in the main app
|
||||
app.include_router(api_router)
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def landing_page():
|
||||
"""
|
||||
Serve the under construction landing page at the root route
|
||||
"""
|
||||
try:
|
||||
# Get the path to the HTML file
|
||||
import os
|
||||
|
||||
html_path = os.path.join(os.path.dirname(__file__), "templates", "index.html")
|
||||
|
||||
with open(html_path, "r", encoding="utf-8") as f:
|
||||
html_content = f.read()
|
||||
|
||||
return HTMLResponse(content=html_content, status_code=200)
|
||||
except FileNotFoundError:
|
||||
# Fallback if HTML file is not found
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>99tales - Under Construction</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
||||
h1 { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🏗️ 99tales</h1>
|
||||
<h2>Under Construction</h2>
|
||||
<p>We're working hard to bring you something amazing!</p>
|
||||
<p><a href="/api">API Documentation</a></p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content, status_code=200)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
111
src/alpine_bits_python/auth.py
Normal file
111
src/alpine_bits_python/auth.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
import secrets
|
||||
from typing import Optional
|
||||
from fastapi import HTTPException, Security, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
import hashlib
|
||||
import hmac
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Security scheme
|
||||
security = HTTPBearer()
|
||||
|
||||
# API Keys - In production, store these in environment variables or a secure database
|
||||
API_KEYS = {
|
||||
# Example API keys - replace with your own secure keys
|
||||
"wix-webhook-key": "sk_live_your_secure_api_key_here",
|
||||
"admin-key": "sk_admin_your_admin_key_here",
|
||||
}
|
||||
|
||||
# Load API keys from environment if available
|
||||
if os.getenv("WIX_API_KEY"):
|
||||
API_KEYS["wix-webhook-key"] = os.getenv("WIX_API_KEY")
|
||||
if os.getenv("ADMIN_API_KEY"):
|
||||
API_KEYS["admin-key"] = os.getenv("ADMIN_API_KEY")
|
||||
|
||||
|
||||
def generate_api_key() -> str:
|
||||
"""Generate a secure API key"""
|
||||
return f"sk_live_{secrets.token_urlsafe(32)}"
|
||||
|
||||
|
||||
def validate_api_key(
|
||||
credentials: HTTPAuthorizationCredentials = Security(security),
|
||||
) -> str:
|
||||
"""
|
||||
Validate API key from Authorization header.
|
||||
Expected format: Authorization: Bearer your_api_key_here
|
||||
"""
|
||||
token = credentials.credentials
|
||||
|
||||
# Check if the token is in our valid API keys
|
||||
for key_name, valid_key in API_KEYS.items():
|
||||
if secrets.compare_digest(token, valid_key):
|
||||
logger.info(f"Valid API key used: {key_name}")
|
||||
return key_name
|
||||
|
||||
logger.warning(f"Invalid API key attempted: {token[:10]}...")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API key",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
def validate_wix_signature(payload: bytes, signature: str, secret: str) -> bool:
|
||||
"""
|
||||
Validate Wix webhook signature for additional security.
|
||||
Wix signs their webhooks with HMAC-SHA256.
|
||||
"""
|
||||
if not signature or not secret:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Remove 'sha256=' prefix if present
|
||||
if signature.startswith("sha256="):
|
||||
signature = signature[7:]
|
||||
|
||||
# Calculate expected signature
|
||||
expected_signature = hmac.new(
|
||||
secret.encode("utf-8"), payload, hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
# Compare signatures securely
|
||||
return secrets.compare_digest(signature, expected_signature)
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating signature: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class APIKeyAuth:
|
||||
"""Simple API key authentication class"""
|
||||
|
||||
def __init__(self, api_keys: dict):
|
||||
self.api_keys = api_keys
|
||||
|
||||
def authenticate(self, api_key: str) -> Optional[str]:
|
||||
"""Authenticate an API key and return the key name if valid"""
|
||||
for key_name, valid_key in self.api_keys.items():
|
||||
if secrets.compare_digest(api_key, valid_key):
|
||||
return key_name
|
||||
return None
|
||||
|
||||
def add_key(self, name: str, key: str):
|
||||
"""Add a new API key"""
|
||||
self.api_keys[name] = key
|
||||
|
||||
def remove_key(self, name: str):
|
||||
"""Remove an API key"""
|
||||
if name in self.api_keys:
|
||||
del self.api_keys[name]
|
||||
|
||||
|
||||
# Initialize auth system
|
||||
auth_system = APIKeyAuth(API_KEYS)
|
||||
97
src/alpine_bits_python/config_loader.py
Normal file
97
src/alpine_bits_python/config_loader.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from annotatedyaml.loader import (
|
||||
HAS_C_LOADER,
|
||||
JSON_TYPE,
|
||||
LoaderType,
|
||||
Secrets,
|
||||
add_constructor,
|
||||
load_yaml as load_annotated_yaml,
|
||||
load_yaml_dict as load_annotated_yaml_dict,
|
||||
parse_yaml as parse_annotated_yaml,
|
||||
secret_yaml as annotated_secret_yaml,
|
||||
)
|
||||
from voluptuous import Schema, Required, All, Length, PREVENT_EXTRA, MultipleInvalid
|
||||
|
||||
# --- Voluptuous schemas ---
|
||||
database_schema = Schema({Required("url"): str}, extra=PREVENT_EXTRA)
|
||||
|
||||
|
||||
hotel_auth_schema = Schema(
|
||||
{
|
||||
Required("hotel_id"): str,
|
||||
Required("hotel_name"): str,
|
||||
Required("username"): str,
|
||||
Required("password"): str,
|
||||
},
|
||||
extra=PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
basic_auth_schema = Schema(All([hotel_auth_schema], Length(min=1)))
|
||||
|
||||
config_schema = Schema(
|
||||
{
|
||||
Required("database"): database_schema,
|
||||
Required("alpine_bits_auth"): basic_auth_schema,
|
||||
},
|
||||
extra=PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
DEFAULT_CONFIG_FILE = "config.yaml"
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(
|
||||
self,
|
||||
config_folder: str | Path = None,
|
||||
config_name: str = DEFAULT_CONFIG_FILE,
|
||||
testing_mode: bool = False,
|
||||
):
|
||||
if config_folder is None:
|
||||
config_folder = os.environ.get("ALPINEBITS_CONFIG_DIR")
|
||||
if not config_folder:
|
||||
config_folder = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "../../config")
|
||||
)
|
||||
if isinstance(config_folder, str):
|
||||
config_folder = Path(config_folder)
|
||||
self.config_folder = config_folder
|
||||
self.config_path = os.path.join(config_folder, config_name)
|
||||
self.secrets = Secrets(config_folder)
|
||||
self.testing_mode = testing_mode
|
||||
self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
stuff = load_annotated_yaml(self.config_path, secrets=self.secrets)
|
||||
try:
|
||||
validated = config_schema(stuff)
|
||||
except MultipleInvalid as e:
|
||||
raise ValueError(f"Config validation error: {e}")
|
||||
self.database = validated["database"]
|
||||
self.basic_auth = validated["alpine_bits_auth"]
|
||||
self.config = validated
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.config.get(key, default)
|
||||
|
||||
@property
|
||||
def db_url(self) -> str:
|
||||
return self.database["url"]
|
||||
|
||||
@property
|
||||
def hotel_id(self) -> str:
|
||||
return self.basic_auth["hotel_id"]
|
||||
|
||||
@property
|
||||
def hotel_name(self) -> str:
|
||||
return self.basic_auth["hotel_name"]
|
||||
|
||||
@property
|
||||
def users(self) -> List[Dict[str, str]]:
|
||||
return self.basic_auth["users"]
|
||||
|
||||
|
||||
# For backward compatibility
|
||||
def load_config():
|
||||
return Config().config
|
||||
82
src/alpine_bits_python/db.py
Normal file
82
src/alpine_bits_python/db.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from sqlalchemy import Column, Integer, String, Date, Boolean, ForeignKey, DateTime
|
||||
from sqlalchemy.orm import declarative_base, relationship
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
import os
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
# Async SQLAlchemy setup
|
||||
def get_database_url(config=None):
|
||||
db_url = None
|
||||
if config and "database" in config and "url" in config["database"]:
|
||||
db_url = config["database"]["url"]
|
||||
if not db_url:
|
||||
db_url = os.environ.get("DATABASE_URL")
|
||||
if not db_url:
|
||||
db_url = "sqlite+aiosqlite:///alpinebits.db"
|
||||
return db_url
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Customer(Base):
|
||||
__tablename__ = "customers"
|
||||
id = Column(Integer, primary_key=True)
|
||||
given_name = Column(String)
|
||||
contact_id = Column(String, unique=True)
|
||||
surname = Column(String)
|
||||
name_prefix = Column(String)
|
||||
email_address = Column(String)
|
||||
phone = Column(String)
|
||||
email_newsletter = Column(Boolean)
|
||||
address_line = Column(String)
|
||||
city_name = Column(String)
|
||||
postal_code = Column(String)
|
||||
country_code = Column(String)
|
||||
gender = Column(String)
|
||||
birth_date = Column(String)
|
||||
language = Column(String)
|
||||
address_catalog = Column(Boolean) # Added for XML
|
||||
name_title = Column(String) # Added for XML
|
||||
reservations = relationship("Reservation", back_populates="customer")
|
||||
|
||||
|
||||
class Reservation(Base):
|
||||
__tablename__ = "reservations"
|
||||
id = Column(Integer, primary_key=True)
|
||||
customer_id = Column(Integer, ForeignKey("customers.id"))
|
||||
form_id = Column(String, unique=True)
|
||||
start_date = Column(Date)
|
||||
end_date = Column(Date)
|
||||
num_adults = Column(Integer)
|
||||
num_children = Column(Integer)
|
||||
children_ages = Column(String) # comma-separated
|
||||
offer = Column(String)
|
||||
utm_comment = Column(String)
|
||||
created_at = Column(DateTime)
|
||||
# Add all UTM fields and user comment for XML
|
||||
utm_source = Column(String)
|
||||
utm_medium = Column(String)
|
||||
utm_campaign = Column(String)
|
||||
utm_term = Column(String)
|
||||
utm_content = Column(String)
|
||||
user_comment = Column(String)
|
||||
fbclid = Column(String)
|
||||
gclid = Column(String)
|
||||
# Add hotel_code and hotel_name for XML
|
||||
hotel_code = Column(String)
|
||||
hotel_name = Column(String)
|
||||
customer = relationship("Customer", back_populates="reservations")
|
||||
|
||||
|
||||
|
||||
class HashedCustomer(Base):
|
||||
__tablename__ = "hashed_customers"
|
||||
id = Column(Integer, primary_key=True)
|
||||
customer_id = Column(Integer)
|
||||
hashed_email = Column(String)
|
||||
hashed_phone = Column(String)
|
||||
hashed_name = Column(String)
|
||||
redacted_at = Column(DateTime)
|
||||
@@ -1,129 +1,339 @@
|
||||
import logging
|
||||
from .alpinebits_guestrequests import ResGuest, RoomStay
|
||||
from .generated import alpinebits as ab
|
||||
from io import BytesIO
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, date
|
||||
import re
|
||||
from xsdata_pydantic.bindings import XmlSerializer
|
||||
from .alpine_bits_helpers import (
|
||||
CustomerData,
|
||||
GuestCountsFactory,
|
||||
HotelReservationIdData,
|
||||
AlpineBitsFactory,
|
||||
OtaMessageType,
|
||||
CommentData,
|
||||
CommentsData,
|
||||
CommentListItemData,
|
||||
)
|
||||
from .generated import alpinebits as ab
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .simplified_access import (
|
||||
from .alpine_bits_helpers import (
|
||||
CommentData,
|
||||
CommentsData,
|
||||
CommentListItemData,
|
||||
CustomerData,
|
||||
|
||||
GuestCountsFactory,
|
||||
HotelReservationIdData,
|
||||
PhoneTechType,
|
||||
AlpineBitsFactory,
|
||||
OtaMessageType
|
||||
OtaMessageType,
|
||||
)
|
||||
|
||||
# DB and config
|
||||
from .db import (
|
||||
Base,
|
||||
Customer as DBCustomer,
|
||||
Reservation as DBReservation,
|
||||
HashedCustomer,
|
||||
get_database_url,
|
||||
)
|
||||
from .config_loader import load_config
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
import asyncio
|
||||
|
||||
def main():
|
||||
# Success - use None instead of object() for cleaner XML output
|
||||
success = None
|
||||
from alpine_bits_python import db
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
async def setup_db(config):
|
||||
DATABASE_URL = get_database_url(config)
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
# Create tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
_LOGGER.info("Database tables checked/created at startup.")
|
||||
|
||||
return engine, AsyncSessionLocal
|
||||
|
||||
|
||||
|
||||
async def main():
|
||||
print("🚀 Starting AlpineBits XML generation script...")
|
||||
# Load config (yaml, annotatedyaml)
|
||||
config = load_config()
|
||||
|
||||
# print config for debugging
|
||||
print("Loaded configuration:")
|
||||
print(json.dumps(config, indent=2))
|
||||
|
||||
# Ensure SQLite DB file exists if using SQLite
|
||||
db_url = config.get("database", {}).get("url", "")
|
||||
if db_url.startswith("sqlite+aiosqlite:///"):
|
||||
db_path = db_url.replace("sqlite+aiosqlite:///", "")
|
||||
db_path = os.path.abspath(db_path)
|
||||
db_dir = os.path.dirname(db_path)
|
||||
if not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
# for now we delete the existing DB for clean testing
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
print(f"Deleted existing SQLite DB at {db_path} for clean testing.")
|
||||
|
||||
# # Ensure DB schema is created (async)
|
||||
|
||||
|
||||
engine, AsyncSessionLocal = await setup_db(config)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
# Load data from JSON file
|
||||
json_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"../../test_data/wix_test_data_20250928_132611.json",
|
||||
)
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
wix_data = json.load(f)
|
||||
data = wix_data["data"]["data"]
|
||||
|
||||
contact_info = data.get("contact", {})
|
||||
first_name = contact_info.get("name", {}).get("first")
|
||||
last_name = contact_info.get("name", {}).get("last")
|
||||
email = contact_info.get("email")
|
||||
phone_number = contact_info.get("phones", [{}])[0].get("e164Phone")
|
||||
locale = contact_info.get("locale", "de-de")
|
||||
contact_id = contact_info.get("contactId")
|
||||
|
||||
name_prefix = data.get("field:anrede")
|
||||
email_newsletter = data.get("field:form_field_5a7b", "") != "Non selezionato"
|
||||
address_line = None
|
||||
city_name = None
|
||||
postal_code = None
|
||||
country_code = None
|
||||
gender = None
|
||||
birth_date = None
|
||||
language = data.get("contact", {}).get("locale", "en")[:2]
|
||||
|
||||
# Dates
|
||||
start_date = (
|
||||
data.get("field:date_picker_a7c8")
|
||||
or data.get("Anreisedatum")
|
||||
or data.get("submissions", [{}])[1].get("value")
|
||||
)
|
||||
end_date = (
|
||||
data.get("field:date_picker_7e65")
|
||||
or data.get("Abreisedatum")
|
||||
or data.get("submissions", [{}])[2].get("value")
|
||||
)
|
||||
|
||||
# Room/guest info
|
||||
num_adults = int(data.get("field:number_7cf5") or 2)
|
||||
num_children = int(data.get("field:anzahl_kinder") or 0)
|
||||
children_ages = []
|
||||
if num_children > 0:
|
||||
for k in data.keys():
|
||||
if k.startswith("field:alter_kind_"):
|
||||
try:
|
||||
age = int(data[k])
|
||||
children_ages.append(age)
|
||||
except ValueError:
|
||||
logging.warning(f"Invalid age value for {k}: {data[k]}")
|
||||
|
||||
# UTM and offer
|
||||
utm_fields = [
|
||||
("utm_Source", "utm_source"),
|
||||
("utm_Medium", "utm_medium"),
|
||||
("utm_Campaign", "utm_campaign"),
|
||||
("utm_Term", "utm_term"),
|
||||
("utm_Content", "utm_content"),
|
||||
]
|
||||
utm_comment_text = []
|
||||
for label, field in utm_fields:
|
||||
val = data.get(f"field:{field}") or data.get(label)
|
||||
if val:
|
||||
utm_comment_text.append(f"{label}: {val}")
|
||||
utm_comment = " | ".join(utm_comment_text) if utm_comment_text else None
|
||||
offer = data.get("field:angebot_auswaehlen")
|
||||
|
||||
# Save all relevant data to DB (including new fields)
|
||||
db_customer = DBCustomer(
|
||||
given_name=first_name,
|
||||
surname=last_name,
|
||||
contact_id=contact_id,
|
||||
name_prefix=name_prefix,
|
||||
email_address=email,
|
||||
phone=phone_number,
|
||||
email_newsletter=email_newsletter,
|
||||
address_line=address_line,
|
||||
city_name=city_name,
|
||||
postal_code=postal_code,
|
||||
country_code=country_code,
|
||||
gender=gender,
|
||||
birth_date=birth_date,
|
||||
language=language,
|
||||
address_catalog=False,
|
||||
name_title=None,
|
||||
)
|
||||
db.add(db_customer)
|
||||
await db.commit()
|
||||
await db.refresh(db_customer)
|
||||
|
||||
db_reservation = DBReservation(
|
||||
customer_id=db_customer.id,
|
||||
form_id=data.get("submissionId"),
|
||||
start_date=date.fromisoformat(start_date) if start_date else None,
|
||||
end_date=date.fromisoformat(end_date) if end_date else None,
|
||||
num_adults=num_adults,
|
||||
num_children=num_children,
|
||||
children_ages=",".join(str(a) for a in children_ages),
|
||||
offer=offer,
|
||||
utm_comment=utm_comment,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
utm_source=data.get("field:utm_source"),
|
||||
utm_medium=data.get("field:utm_medium"),
|
||||
utm_campaign=data.get("field:utm_campaign"),
|
||||
utm_term=data.get("field:utm_term"),
|
||||
utm_content=data.get("field:utm_content"),
|
||||
user_comment=data.get("field:long_answer_3524", ""),
|
||||
fbclid=data.get("field:fbclid"),
|
||||
gclid=data.get("field:gclid"),
|
||||
hotel_code="123",
|
||||
hotel_name="Frangart Inn",
|
||||
)
|
||||
db.add(db_reservation)
|
||||
await db.commit()
|
||||
await db.refresh(db_reservation)
|
||||
|
||||
# Now read back from DB
|
||||
customer = await db.get(DBCustomer, db_reservation.customer_id)
|
||||
reservation = await db.get(DBReservation, db_reservation.id)
|
||||
|
||||
# Generate XML from DB data
|
||||
create_xml_from_db(customer, reservation)
|
||||
|
||||
await db.close()
|
||||
|
||||
|
||||
def create_xml_from_db(customer: DBCustomer, reservation: DBReservation):
|
||||
|
||||
|
||||
# Prepare data for XML
|
||||
phone_numbers = [(customer.phone, PhoneTechType.MOBILE)] if customer.phone else []
|
||||
customer_data = CustomerData(
|
||||
given_name=customer.given_name,
|
||||
surname=customer.surname,
|
||||
name_prefix=customer.name_prefix,
|
||||
name_title=customer.name_title,
|
||||
phone_numbers=phone_numbers,
|
||||
email_address=customer.email_address,
|
||||
email_newsletter=customer.email_newsletter,
|
||||
address_line=customer.address_line,
|
||||
city_name=customer.city_name,
|
||||
postal_code=customer.postal_code,
|
||||
country_code=customer.country_code,
|
||||
address_catalog=customer.address_catalog,
|
||||
gender=customer.gender,
|
||||
birth_date=customer.birth_date,
|
||||
language=customer.language,
|
||||
)
|
||||
alpine_bits_factory = AlpineBitsFactory()
|
||||
res_guests = alpine_bits_factory.create_res_guests(
|
||||
customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
# Guest counts
|
||||
children_ages = [int(a) for a in reservation.children_ages.split(",") if a]
|
||||
guest_counts = GuestCountsFactory.create_retrieve_guest_counts(
|
||||
reservation.num_adults, children_ages
|
||||
)
|
||||
|
||||
# UniqueID
|
||||
unique_id = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.UniqueId(
|
||||
type_value=ab.UniqueIdType2.VALUE_14, id="6b34fe24ac2ff811"
|
||||
)
|
||||
|
||||
# TimeSpan - use the actual nested class
|
||||
|
||||
start_date_window = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.TimeSpan.StartDateWindow(
|
||||
earliest_date="2024-10-01", latest_date="2024-10-02"
|
||||
type_value=ab.UniqueIdType2.VALUE_14, id=reservation.form_id
|
||||
)
|
||||
|
||||
# TimeSpan
|
||||
time_span = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay.TimeSpan(
|
||||
start_date_window=start_date_window
|
||||
start=reservation.start_date.isoformat() if reservation.start_date else None,
|
||||
end=reservation.end_date.isoformat() if reservation.end_date else None,
|
||||
)
|
||||
|
||||
# RoomStay with TimeSpan
|
||||
room_stay = (
|
||||
ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays.RoomStay(
|
||||
time_span=time_span
|
||||
time_span=time_span,
|
||||
guest_counts=guest_counts,
|
||||
)
|
||||
)
|
||||
room_stays = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.RoomStays(
|
||||
room_stay=[room_stay]
|
||||
room_stay=[room_stay],
|
||||
)
|
||||
|
||||
customer_data = CustomerData(
|
||||
given_name="John",
|
||||
surname="Doe",
|
||||
name_prefix="Mr.",
|
||||
phone_numbers=[
|
||||
("+1234567890", PhoneTechType.MOBILE), # Phone number with type
|
||||
("+0987654321", None), # Phone number without type
|
||||
],
|
||||
email_address="john.doe@example.com",
|
||||
email_newsletter=True,
|
||||
address_line="123 Main Street",
|
||||
city_name="Anytown",
|
||||
postal_code="12345",
|
||||
country_code="US",
|
||||
address_catalog=False,
|
||||
gender="Male",
|
||||
birth_date="1980-01-01",
|
||||
language="en",
|
||||
)
|
||||
|
||||
alpine_bits_factory = AlpineBitsFactory()
|
||||
|
||||
res_guests = alpine_bits_factory.create_res_guests(customer_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
# HotelReservationId
|
||||
hotel_res_id_data = HotelReservationIdData(
|
||||
res_id_type="13",
|
||||
res_id_value=None,
|
||||
res_id_value=reservation.fbclid or reservation.gclid,
|
||||
res_id_source=None,
|
||||
res_id_source_context="99tales",
|
||||
)
|
||||
# Create HotelReservationId using the factory
|
||||
hotel_res_id = alpine_bits_factory.create(hotel_res_id_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
# Use the actual nested HotelReservationIds class
|
||||
hotel_res_id = alpine_bits_factory.create(
|
||||
hotel_res_id_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
hotel_res_ids = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.HotelReservationIds(
|
||||
hotel_reservation_id=[hotel_res_id]
|
||||
)
|
||||
|
||||
# Basic property info
|
||||
basic_property_info = ab.OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo.BasicPropertyInfo(
|
||||
hotel_code="123", hotel_name="Frangart Inn"
|
||||
hotel_code=reservation.hotel_code,
|
||||
hotel_name=reservation.hotel_name,
|
||||
)
|
||||
|
||||
comment = CommentData(
|
||||
name= ab.CommentName2.CUSTOMER_COMMENT,
|
||||
text="This is a sample comment.",
|
||||
list_items=[CommentListItemData(
|
||||
value="Landing page comment",
|
||||
language="en",
|
||||
list_item="1",
|
||||
)],
|
||||
|
||||
|
||||
# Comments
|
||||
offer_comment = CommentData(
|
||||
name=ab.CommentName2.ADDITIONAL_INFO,
|
||||
text="Angebot/Offerta",
|
||||
list_items=[
|
||||
CommentListItemData(
|
||||
value=reservation.offer,
|
||||
language=customer.language,
|
||||
list_item="1",
|
||||
)
|
||||
],
|
||||
)
|
||||
comment = None
|
||||
if reservation.user_comment:
|
||||
comment = CommentData(
|
||||
name=ab.CommentName2.CUSTOMER_COMMENT,
|
||||
text=reservation.user_comment,
|
||||
list_items=[
|
||||
CommentListItemData(
|
||||
value="Landing page comment",
|
||||
language=customer.language,
|
||||
list_item="1",
|
||||
)
|
||||
],
|
||||
)
|
||||
comments = [offer_comment, comment] if comment else [offer_comment]
|
||||
comments_data = CommentsData(comments=comments)
|
||||
comments_xml = alpine_bits_factory.create(comments_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
comment2 = CommentData(
|
||||
name= ab.CommentName2.ADDITIONAL_INFO,
|
||||
text="This is a special request comment.",
|
||||
|
||||
)
|
||||
|
||||
comments_data = CommentsData(comments=[comment, comment2])
|
||||
|
||||
|
||||
comments = alpine_bits_factory.create(comments_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
|
||||
|
||||
# ResGlobalInfo
|
||||
res_global_info = (
|
||||
ab.OtaResRetrieveRs.ReservationsList.HotelReservation.ResGlobalInfo(
|
||||
hotel_reservation_ids=hotel_res_ids, basic_property_info=basic_property_info, comments=comments
|
||||
hotel_reservation_ids=hotel_res_ids,
|
||||
basic_property_info=basic_property_info,
|
||||
comments=comments_xml,
|
||||
)
|
||||
)
|
||||
|
||||
# Hotel Reservation
|
||||
hotel_reservation = ab.OtaResRetrieveRs.ReservationsList.HotelReservation(
|
||||
create_date_time=datetime.now(timezone.utc).isoformat(),
|
||||
res_status=ab.HotelReservationResStatus.REQUESTED,
|
||||
@@ -133,63 +343,45 @@ def main():
|
||||
res_guests=res_guests,
|
||||
res_global_info=res_global_info,
|
||||
)
|
||||
|
||||
reservations_list = ab.OtaResRetrieveRs.ReservationsList(
|
||||
hotel_reservation=[hotel_reservation]
|
||||
)
|
||||
|
||||
# Root element
|
||||
ota_res_retrieve_rs = ab.OtaResRetrieveRs(
|
||||
version="7.000", success=success, reservations_list=reservations_list
|
||||
version="7.000", success=None, reservations_list=reservations_list
|
||||
)
|
||||
|
||||
# Serialize using Pydantic's model_dump and convert to XML
|
||||
# Serialize to XML
|
||||
try:
|
||||
# First validate the model
|
||||
ota_res_retrieve_rs.model_validate(ota_res_retrieve_rs.model_dump())
|
||||
print("✅ Pydantic validation successful!")
|
||||
|
||||
# For XML serialization with Pydantic models, we need to use xsdata-pydantic serializer
|
||||
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
||||
from xsdata_pydantic.bindings import XmlSerializer
|
||||
|
||||
config = SerializerConfig(
|
||||
pretty_print=True, xml_declaration=True, encoding="UTF-8"
|
||||
)
|
||||
|
||||
serializer = XmlSerializer(config=config)
|
||||
|
||||
# Use ns_map to control namespace prefixes - set default namespace
|
||||
ns_map = {None: "http://www.opentravel.org/OTA/2003/05"}
|
||||
xml_string = serializer.render(ota_res_retrieve_rs, ns_map=ns_map)
|
||||
|
||||
with open("output.xml", "w", encoding="utf-8") as outfile:
|
||||
outfile.write(xml_string)
|
||||
|
||||
print("✅ XML serialization successful!")
|
||||
print(f"Generated XML written to output.xml")
|
||||
|
||||
# Also print the pretty formatted XML to console
|
||||
print("\n📄 Generated XML:")
|
||||
print(xml_string)
|
||||
|
||||
# Test parsing back
|
||||
from xsdata_pydantic.bindings import XmlParser
|
||||
|
||||
parser = XmlParser()
|
||||
|
||||
with open("output.xml", "r", encoding="utf-8") as infile:
|
||||
xml_content = infile.read()
|
||||
|
||||
parsed_result = parser.from_string(xml_content, ab.OtaResRetrieveRs)
|
||||
|
||||
print("✅ Round-trip validation successful!")
|
||||
print(
|
||||
f"Parsed reservation status: {parsed_result.reservations_list.hotel_reservation[0].res_status}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Validation/Serialization failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
asyncio.run(main())
|
||||
|
||||
73
src/alpine_bits_python/models.py
Normal file
73
src/alpine_bits_python/models.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from typing import Dict, List, Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AlpineBitsHandshakeRequest(BaseModel):
|
||||
"""Model for AlpineBits handshake request data"""
|
||||
|
||||
action: str = Field(
|
||||
..., description="Action parameter, typically 'OTA_Ping:Handshaking'"
|
||||
)
|
||||
request_xml: Optional[str] = Field(None, description="XML request document")
|
||||
|
||||
|
||||
class ContactName(BaseModel):
|
||||
"""Contact name structure"""
|
||||
|
||||
first: Optional[str] = None
|
||||
last: Optional[str] = None
|
||||
|
||||
|
||||
class ContactAddress(BaseModel):
|
||||
"""Contact address structure"""
|
||||
|
||||
street: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
postalCode: Optional[str] = None
|
||||
|
||||
|
||||
class Contact(BaseModel):
|
||||
"""Contact information from Wix form"""
|
||||
|
||||
name: Optional[ContactName] = None
|
||||
email: Optional[str] = None
|
||||
locale: Optional[str] = None
|
||||
company: Optional[str] = None
|
||||
birthdate: Optional[str] = None
|
||||
labelKeys: Optional[Dict[str, Any]] = None
|
||||
contactId: Optional[str] = None
|
||||
address: Optional[ContactAddress] = None
|
||||
jobTitle: Optional[str] = None
|
||||
imageUrl: Optional[str] = None
|
||||
updatedDate: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
createdDate: Optional[str] = None
|
||||
|
||||
|
||||
class SubmissionPdf(BaseModel):
|
||||
"""PDF submission structure"""
|
||||
|
||||
url: Optional[str] = None
|
||||
filename: Optional[str] = None
|
||||
|
||||
|
||||
class WixFormSubmission(BaseModel):
|
||||
"""Model for Wix form submission data"""
|
||||
|
||||
formName: str
|
||||
submissions: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
submissionTime: str
|
||||
formFieldMask: List[str] = Field(default_factory=list)
|
||||
submissionId: str
|
||||
contactId: str
|
||||
submissionsLink: str
|
||||
submissionPdf: Optional[SubmissionPdf] = None
|
||||
formId: str
|
||||
contact: Optional[Contact] = None
|
||||
|
||||
# Dynamic form fields - these will capture all field:* entries
|
||||
class Config:
|
||||
extra = "allow" # Allow additional fields not defined in the model
|
||||
101
src/alpine_bits_python/rate_limit.py
Normal file
101
src/alpine_bits_python/rate_limit.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from fastapi import Request
|
||||
import redis
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Rate limiting configuration
|
||||
DEFAULT_RATE_LIMIT = "10/minute" # 10 requests per minute per IP
|
||||
WEBHOOK_RATE_LIMIT = "60/minute" # 60 webhook requests per minute per IP
|
||||
BURST_RATE_LIMIT = "3/second" # Max 3 requests per second per IP
|
||||
|
||||
# Redis configuration for distributed rate limiting (optional)
|
||||
REDIS_URL = os.getenv("REDIS_URL", None)
|
||||
|
||||
|
||||
def get_remote_address_with_forwarded(request: Request):
|
||||
"""
|
||||
Get client IP address, considering forwarded headers from proxies/load balancers
|
||||
"""
|
||||
# Check for forwarded headers (common in production behind proxies)
|
||||
forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if forwarded_for:
|
||||
# Take the first IP in the chain
|
||||
return forwarded_for.split(",")[0].strip()
|
||||
|
||||
real_ip = request.headers.get("X-Real-IP")
|
||||
if real_ip:
|
||||
return real_ip
|
||||
|
||||
# Fallback to direct connection IP
|
||||
return get_remote_address(request)
|
||||
|
||||
|
||||
# Initialize limiter
|
||||
if REDIS_URL:
|
||||
# Use Redis for distributed rate limiting (recommended for production)
|
||||
try:
|
||||
import redis
|
||||
|
||||
redis_client = redis.from_url(REDIS_URL)
|
||||
limiter = Limiter(
|
||||
key_func=get_remote_address_with_forwarded, storage_uri=REDIS_URL
|
||||
)
|
||||
logger.info("Rate limiting initialized with Redis backend")
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to connect to Redis: {e}. Using in-memory rate limiting."
|
||||
)
|
||||
limiter = Limiter(key_func=get_remote_address_with_forwarded)
|
||||
else:
|
||||
# Use in-memory rate limiting (fine for single instance)
|
||||
limiter = Limiter(key_func=get_remote_address_with_forwarded)
|
||||
logger.info("Rate limiting initialized with in-memory backend")
|
||||
|
||||
|
||||
def get_api_key_identifier(request: Request) -> str:
|
||||
"""
|
||||
Get identifier for rate limiting based on API key if available, otherwise IP
|
||||
This allows different rate limits per API key
|
||||
"""
|
||||
# Try to get API key from Authorization header
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
api_key = auth_header[7:] # Remove "Bearer " prefix
|
||||
# Use first 10 chars of API key as identifier (don't log full key)
|
||||
return f"api_key:{api_key[:10]}"
|
||||
|
||||
# Fallback to IP address
|
||||
return f"ip:{get_remote_address_with_forwarded(request)}"
|
||||
|
||||
|
||||
# Custom rate limit key function for API key based limiting
|
||||
def api_key_rate_limit_key(request: Request):
|
||||
return get_api_key_identifier(request)
|
||||
|
||||
|
||||
# Rate limiting decorators for different endpoint types
|
||||
webhook_limiter = Limiter(
|
||||
key_func=api_key_rate_limit_key, storage_uri=REDIS_URL if REDIS_URL else None
|
||||
)
|
||||
|
||||
|
||||
# Custom rate limit exceeded handler
|
||||
def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
||||
"""Custom handler for rate limit exceeded"""
|
||||
logger.warning(
|
||||
f"Rate limit exceeded for {get_remote_address_with_forwarded(request)}: "
|
||||
f"{exc.detail}"
|
||||
)
|
||||
|
||||
response = _rate_limit_exceeded_handler(request, exc)
|
||||
|
||||
# Add custom headers
|
||||
response.headers["X-RateLimit-Limit"] = str(exc.retry_after)
|
||||
response.headers["X-RateLimit-Retry-After"] = str(exc.retry_after)
|
||||
|
||||
return response
|
||||
2
src/alpine_bits_python/reservations.py
Normal file
2
src/alpine_bits_python/reservations.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def parse_form(form: dict):
|
||||
pass
|
||||
22
src/alpine_bits_python/run_api.py
Normal file
22
src/alpine_bits_python/run_api.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Startup script for the Wix Form Handler API
|
||||
"""
|
||||
|
||||
import os
|
||||
import uvicorn
|
||||
from .api import app
|
||||
|
||||
if __name__ == "__main__":
|
||||
db_path = "alpinebits.db" # Adjust path if needed
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
print(f"Deleted database file: {db_path}")
|
||||
|
||||
uvicorn.run(
|
||||
"alpine_bits_python.api:app",
|
||||
host="0.0.0.0",
|
||||
port=8080,
|
||||
reload=True, # Enable auto-reload during development
|
||||
log_level="info",
|
||||
)
|
||||
135
src/alpine_bits_python/scripts/setup_security.py
Normal file
135
src/alpine_bits_python/scripts/setup_security.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Configuration and setup script for the Wix Form Handler API
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import secrets
|
||||
|
||||
# Add parent directory to path to import from src
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from alpine_bits_python.auth import generate_api_key
|
||||
|
||||
|
||||
def generate_secure_keys():
|
||||
"""Generate secure API keys for the application"""
|
||||
|
||||
print("🔐 Generating Secure API Keys")
|
||||
print("=" * 50)
|
||||
|
||||
# Generate API keys
|
||||
wix_api_key = generate_api_key()
|
||||
admin_api_key = generate_api_key()
|
||||
webhook_secret = secrets.token_urlsafe(32)
|
||||
|
||||
print(f"🔑 Wix Webhook API Key: {wix_api_key}")
|
||||
print(f"🔐 Admin API Key: {admin_api_key}")
|
||||
print(f"🔒 Webhook Secret: {webhook_secret}")
|
||||
|
||||
print("\n📋 Environment Variables")
|
||||
print("-" * 30)
|
||||
print(f"export WIX_API_KEY='{wix_api_key}'")
|
||||
print(f"export ADMIN_API_KEY='{admin_api_key}'")
|
||||
print(f"export WIX_WEBHOOK_SECRET='{webhook_secret}'")
|
||||
print(f"export REDIS_URL='redis://localhost:6379' # Optional for production")
|
||||
|
||||
print("\n🔧 .env File Content")
|
||||
print("-" * 20)
|
||||
print(f"WIX_API_KEY={wix_api_key}")
|
||||
print(f"ADMIN_API_KEY={admin_api_key}")
|
||||
print(f"WIX_WEBHOOK_SECRET={webhook_secret}")
|
||||
print("REDIS_URL=redis://localhost:6379")
|
||||
|
||||
# Optionally write to .env file
|
||||
create_env = input("\n❓ Create .env file? (y/n): ").lower().strip()
|
||||
if create_env == "y":
|
||||
# Create .env in the project root (two levels up from scripts)
|
||||
env_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env"
|
||||
)
|
||||
with open(env_path, "w") as f:
|
||||
f.write(f"WIX_API_KEY={wix_api_key}\n")
|
||||
f.write(f"ADMIN_API_KEY={admin_api_key}\n")
|
||||
f.write(f"WIX_WEBHOOK_SECRET={webhook_secret}\n")
|
||||
f.write("REDIS_URL=redis://localhost:6379\n")
|
||||
print(f"✅ .env file created at {env_path}!")
|
||||
print("⚠️ Add .env to your .gitignore file!")
|
||||
|
||||
print("\n🌐 Wix Configuration")
|
||||
print("-" * 20)
|
||||
print("1. In your Wix site, go to Settings > Webhooks")
|
||||
print("2. Add webhook URL: https://yourdomain.com/webhook/wix-form")
|
||||
print("3. Add custom header: Authorization: Bearer " + wix_api_key)
|
||||
print("4. Optionally configure webhook signature with the secret above")
|
||||
|
||||
return {
|
||||
"wix_api_key": wix_api_key,
|
||||
"admin_api_key": admin_api_key,
|
||||
"webhook_secret": webhook_secret,
|
||||
}
|
||||
|
||||
|
||||
def check_security_setup():
|
||||
"""Check current security configuration"""
|
||||
|
||||
print("🔍 Security Configuration Check")
|
||||
print("=" * 40)
|
||||
|
||||
# Check environment variables
|
||||
wix_key = os.getenv("WIX_API_KEY")
|
||||
admin_key = os.getenv("ADMIN_API_KEY")
|
||||
webhook_secret = os.getenv("WIX_WEBHOOK_SECRET")
|
||||
redis_url = os.getenv("REDIS_URL")
|
||||
|
||||
print("Environment Variables:")
|
||||
print(f" WIX_API_KEY: {'✅ Set' if wix_key else '❌ Not set'}")
|
||||
print(f" ADMIN_API_KEY: {'✅ Set' if admin_key else '❌ Not set'}")
|
||||
print(f" WIX_WEBHOOK_SECRET: {'✅ Set' if webhook_secret else '❌ Not set'}")
|
||||
print(f" REDIS_URL: {'✅ Set' if redis_url else '⚠️ Optional (using in-memory)'}")
|
||||
|
||||
# Security recommendations
|
||||
print("\n🛡️ Security Recommendations:")
|
||||
if not wix_key:
|
||||
print(" ❌ Set WIX_API_KEY environment variable")
|
||||
else:
|
||||
if len(wix_key) < 32:
|
||||
print(" ⚠️ WIX_API_KEY should be longer for better security")
|
||||
else:
|
||||
print(" ✅ WIX_API_KEY looks secure")
|
||||
|
||||
if not admin_key:
|
||||
print(" ❌ Set ADMIN_API_KEY environment variable")
|
||||
elif wix_key and admin_key == wix_key:
|
||||
print(" ❌ Admin and Wix keys should be different")
|
||||
else:
|
||||
print(" ✅ ADMIN_API_KEY configured")
|
||||
|
||||
if not webhook_secret:
|
||||
print(" ⚠️ Consider setting WIX_WEBHOOK_SECRET for signature validation")
|
||||
else:
|
||||
print(" ✅ Webhook signature validation enabled")
|
||||
|
||||
print("\n🚀 Production Checklist:")
|
||||
print(" - Use HTTPS in production")
|
||||
print(" - Set up Redis for distributed rate limiting")
|
||||
print(" - Configure proper CORS origins")
|
||||
print(" - Set up monitoring and logging")
|
||||
print(" - Regular key rotation")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔐 Wix Form Handler API - Security Setup")
|
||||
print("=" * 50)
|
||||
|
||||
choice = input(
|
||||
"Choose an option:\n1. Generate new API keys\n2. Check current setup\n\nEnter choice (1 or 2): "
|
||||
).strip()
|
||||
|
||||
if choice == "1":
|
||||
generate_secure_keys()
|
||||
elif choice == "2":
|
||||
check_security_setup()
|
||||
else:
|
||||
print("Invalid choice. Please run again and choose 1 or 2.")
|
||||
221
src/alpine_bits_python/scripts/test_api.py
Normal file
221
src/alpine_bits_python/scripts/test_api.py
Normal file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the Secure Wix Form Handler API
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# Add parent directory to path to import from src
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# API Configuration
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
# API Keys for testing - replace with your actual keys
|
||||
TEST_API_KEY = os.getenv("WIX_API_KEY", "sk_live_your_secure_api_key_here")
|
||||
ADMIN_API_KEY = os.getenv("ADMIN_API_KEY", "sk_admin_your_admin_key_here")
|
||||
|
||||
# Sample Wix form data based on your example
|
||||
SAMPLE_WIX_DATA = {
|
||||
"formName": "Contact Form",
|
||||
"submissions": [],
|
||||
"submissionTime": "2024-03-20T10:30:00+00:00",
|
||||
"formFieldMask": ["email", "name", "phone"],
|
||||
"submissionId": "test-submission-123",
|
||||
"contactId": "test-contact-456",
|
||||
"submissionsLink": "https://www.wix.app/forms/test-form/submissions",
|
||||
"submissionPdf": {
|
||||
"url": "https://example.com/submission.pdf",
|
||||
"filename": "submission.pdf",
|
||||
},
|
||||
"formId": "test-form-789",
|
||||
"field:email_5139": "test@example.com",
|
||||
"field:first_name_abae": "John",
|
||||
"field:last_name_d97c": "Doe",
|
||||
"field:phone_4c77": "+1234567890",
|
||||
"field:anrede": "Herr",
|
||||
"field:anzahl_kinder": "2",
|
||||
"field:alter_kind_3": "8",
|
||||
"field:alter_kind_4": "12",
|
||||
"field:long_answer_3524": "This is a long answer field with more details about the inquiry.",
|
||||
"contact": {
|
||||
"name": {"first": "John", "last": "Doe"},
|
||||
"email": "test@example.com",
|
||||
"locale": "de",
|
||||
"company": "Test Company",
|
||||
"birthdate": "1985-05-15",
|
||||
"labelKeys": {},
|
||||
"contactId": "test-contact-456",
|
||||
"address": {
|
||||
"street": "Test Street 123",
|
||||
"city": "Test City",
|
||||
"country": "Germany",
|
||||
"postalCode": "12345",
|
||||
},
|
||||
"jobTitle": "Manager",
|
||||
"phone": "+1234567890",
|
||||
"createdDate": "2024-03-20T10:00:00.000Z",
|
||||
"updatedDate": "2024-03-20T10:30:00.000Z",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def test_api():
|
||||
"""Test the API endpoints with authentication"""
|
||||
|
||||
headers_with_auth = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {TEST_API_KEY}",
|
||||
}
|
||||
|
||||
admin_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {ADMIN_API_KEY}",
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
# Test health endpoint (no auth required)
|
||||
print("1. Testing health endpoint (no auth)...")
|
||||
try:
|
||||
async with session.get(f"{BASE_URL}/api/health") as response:
|
||||
result = await response.json()
|
||||
print(f" ✅ Health check: {response.status} - {result.get('status')}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Health check failed: {e}")
|
||||
|
||||
# Test root endpoint (no auth required)
|
||||
print("\n2. Testing root endpoint (no auth)...")
|
||||
try:
|
||||
async with session.get(f"{BASE_URL}/api/") as response:
|
||||
result = await response.json()
|
||||
print(f" ✅ Root: {response.status} - {result.get('message')}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Root endpoint failed: {e}")
|
||||
|
||||
# Test webhook endpoint without auth (should fail)
|
||||
print("\n3. Testing webhook endpoint WITHOUT auth (should fail)...")
|
||||
try:
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/webhook/wix-form",
|
||||
json=SAMPLE_WIX_DATA,
|
||||
headers={"Content-Type": "application/json"},
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 401:
|
||||
print(
|
||||
f" ✅ Correctly rejected: {response.status} - {result.get('detail')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Unexpected response: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Test failed: {e}")
|
||||
|
||||
# Test webhook endpoint with valid auth
|
||||
print("\n4. Testing webhook endpoint WITH valid auth...")
|
||||
try:
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/webhook/wix-form",
|
||||
json=SAMPLE_WIX_DATA,
|
||||
headers=headers_with_auth,
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 200:
|
||||
print(
|
||||
f" ✅ Webhook success: {response.status} - {result.get('status')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Webhook failed: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Webhook test failed: {e}")
|
||||
|
||||
# Test test endpoint with auth
|
||||
print("\n5. Testing simple test endpoint WITH auth...")
|
||||
try:
|
||||
async with session.post(
|
||||
f"{BASE_URL}/api/webhook/wix-form/test",
|
||||
json={"test": "data", "timestamp": datetime.now().isoformat()},
|
||||
headers=headers_with_auth,
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 200:
|
||||
print(
|
||||
f" ✅ Test endpoint: {response.status} - {result.get('status')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Test endpoint failed: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Test endpoint failed: {e}")
|
||||
|
||||
# Test rate limiting by making multiple rapid requests
|
||||
print("\n6. Testing rate limiting (making 5 rapid requests)...")
|
||||
rate_limit_test_count = 0
|
||||
for i in range(5):
|
||||
try:
|
||||
async with session.get(f"{BASE_URL}/api/health") as response:
|
||||
if response.status == 200:
|
||||
rate_limit_test_count += 1
|
||||
elif response.status == 429:
|
||||
print(f" ✅ Rate limit triggered on request {i + 1}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f" ❌ Rate limit test failed: {e}")
|
||||
break
|
||||
|
||||
if rate_limit_test_count == 5:
|
||||
print(" ℹ️ No rate limit reached (normal for low request volume)")
|
||||
|
||||
# Test admin endpoint (if admin key is configured)
|
||||
print("\n7. Testing admin stats endpoint...")
|
||||
try:
|
||||
async with session.get(
|
||||
f"{BASE_URL}/api/admin/stats", headers=admin_headers
|
||||
) as response:
|
||||
result = await response.json()
|
||||
if response.status == 200:
|
||||
print(
|
||||
f" ✅ Admin stats: {response.status} - {result.get('status')}"
|
||||
)
|
||||
elif response.status == 401:
|
||||
print(
|
||||
f" ⚠️ Admin access denied (API key not configured): {result.get('detail')}"
|
||||
)
|
||||
else:
|
||||
print(f" ❌ Admin endpoint failed: {response.status} - {result}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Admin test failed: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔒 Testing Secure Wix Form Handler API...")
|
||||
print("=" * 60)
|
||||
print("📍 API URL:", BASE_URL)
|
||||
print(
|
||||
"🔑 Using API Key:",
|
||||
TEST_API_KEY[:20] + "..." if len(TEST_API_KEY) > 20 else TEST_API_KEY,
|
||||
)
|
||||
print(
|
||||
"🔐 Using Admin Key:",
|
||||
ADMIN_API_KEY[:20] + "..." if len(ADMIN_API_KEY) > 20 else ADMIN_API_KEY,
|
||||
)
|
||||
print("=" * 60)
|
||||
print("Make sure the API is running with: python3 run_api.py")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
asyncio.run(test_api())
|
||||
print("\n" + "=" * 60)
|
||||
print("✅ Testing completed!")
|
||||
print("\n📋 Quick Setup Reminder:")
|
||||
print("1. Set environment variables:")
|
||||
print(" export WIX_API_KEY='your_secure_api_key'")
|
||||
print(" export ADMIN_API_KEY='your_admin_key'")
|
||||
print("2. Configure Wix webhook URL: https://yourdomain.com/webhook/wix-form")
|
||||
print("3. Add Authorization header: Bearer your_api_key")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error testing API: {e}")
|
||||
print("Make sure the API server is running!")
|
||||
108
src/alpine_bits_python/templates/index.html
Normal file
108
src/alpine_bits_python/templates/index.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>99 Tales - Under Construction</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
padding: 2rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.construction-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
margin-top: 2rem;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.api-link {
|
||||
display: inline-block;
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.api-link:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="construction-icon">🏗️</div>
|
||||
<h1>99 Tales</h1>
|
||||
<div class="subtitle">Coming Soon</div>
|
||||
<div class="description">
|
||||
We're working hard to bring you something amazing. Our team is putting the finishing touches on an exciting new experience.
|
||||
</div>
|
||||
<div class="description">
|
||||
Thank you for your patience while we build something special for you.
|
||||
</div>
|
||||
<a href="/api" class="api-link">API Documentation</a>
|
||||
<div class="contact-info">
|
||||
Check back soon for updates!
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +1 @@
|
||||
"""Utility functions for alpine_bits_python."""
|
||||
"""Utility functions for alpine_bits_python."""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Entry point for util package."""
|
||||
|
||||
from .handshake_util import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -2,26 +2,22 @@ from ..generated.alpinebits import OtaPingRq, OtaPingRs
|
||||
from xsdata_pydantic.bindings import XmlParser
|
||||
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
# test parsing a ping request sample
|
||||
|
||||
path = "AlpineBits-HotelData-2024-10/files/samples/Handshake/Handshake-OTA_PingRS.xml"
|
||||
path = (
|
||||
"AlpineBits-HotelData-2024-10/files/samples/Handshake/Handshake-OTA_PingRS.xml"
|
||||
)
|
||||
|
||||
with open(
|
||||
path, "r", encoding="utf-8") as f:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
xml = f.read()
|
||||
|
||||
# Parse the XML into the request object
|
||||
|
||||
# Test parsing back
|
||||
|
||||
# Test parsing back
|
||||
|
||||
parser = XmlParser()
|
||||
|
||||
|
||||
|
||||
parsed_result = parser.from_string(xml, OtaPingRs)
|
||||
|
||||
print(parsed_result.echo_data)
|
||||
@@ -34,19 +30,14 @@ def main():
|
||||
|
||||
print(warning.content[0])
|
||||
|
||||
|
||||
|
||||
|
||||
# save json in echo_data to file with indents
|
||||
output_path = "echo_data_response.json"
|
||||
with open(output_path, "w", encoding="utf-8") as out_f:
|
||||
import json
|
||||
|
||||
json.dump(json.loads(parsed_result.echo_data), out_f, indent=4)
|
||||
print(f"Saved echo_data json to {output_path}")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
||||
main()
|
||||
main()
|
||||
|
||||
14
start_api.py
Normal file
14
start_api.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convenience launcher for the Wix Form Handler API
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# Change to src directory
|
||||
src_dir = os.path.join(os.path.dirname(__file__), "src/alpine_bits_python")
|
||||
|
||||
# Run the API using uv
|
||||
if __name__ == "__main__":
|
||||
subprocess.run(["uv", "run", "python", os.path.join(src_dir, "run_api.py")])
|
||||
@@ -5,57 +5,63 @@ discovers implemented vs unimplemented actions.
|
||||
"""
|
||||
|
||||
from alpine_bits_python.alpinebits_server import (
|
||||
ServerCapabilities,
|
||||
AlpineBitsAction,
|
||||
AlpineBitsActionName,
|
||||
Version,
|
||||
ServerCapabilities,
|
||||
AlpineBitsAction,
|
||||
AlpineBitsActionName,
|
||||
Version,
|
||||
AlpineBitsResponse,
|
||||
HttpStatusCode
|
||||
HttpStatusCode,
|
||||
)
|
||||
import asyncio
|
||||
|
||||
|
||||
class NewImplementedAction(AlpineBitsAction):
|
||||
"""A new action that IS implemented."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.name = AlpineBitsActionName.OTA_HOTEL_DESCRIPTIVE_INFO_INFO
|
||||
self.version = Version.V2024_10
|
||||
|
||||
async def handle(self, action: str, request_xml: str, version: Version) -> AlpineBitsResponse:
|
||||
|
||||
async def handle(
|
||||
self, action: str, request_xml: str, version: Version
|
||||
) -> AlpineBitsResponse:
|
||||
"""This action is implemented."""
|
||||
return AlpineBitsResponse("Implemented!", HttpStatusCode.OK)
|
||||
|
||||
|
||||
class NewUnimplementedAction(AlpineBitsAction):
|
||||
"""A new action that is NOT implemented (no handle override)."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.name = AlpineBitsActionName.OTA_HOTEL_DESCRIPTIVE_CONTENT_NOTIF_INFO
|
||||
self.version = Version.V2024_10
|
||||
|
||||
|
||||
# Notice: No handle method override - will use default "not implemented"
|
||||
|
||||
|
||||
async def main():
|
||||
print("🔍 Testing Action Discovery Logic")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# Create capabilities and see what gets discovered
|
||||
capabilities = ServerCapabilities()
|
||||
|
||||
|
||||
print("📋 Actions found by discovery:")
|
||||
for action_name in capabilities.get_supported_actions():
|
||||
print(f" ✅ {action_name}")
|
||||
|
||||
|
||||
print(f"\n📊 Total discovered: {len(capabilities.get_supported_actions())}")
|
||||
|
||||
|
||||
# Test the new implemented action
|
||||
implemented_action = NewImplementedAction()
|
||||
result = await implemented_action.handle("test", "<xml/>", Version.V2024_10)
|
||||
print(f"\n🟢 NewImplementedAction result: {result.xml_content}")
|
||||
|
||||
|
||||
# Test the unimplemented action (should use default behavior)
|
||||
unimplemented_action = NewUnimplementedAction()
|
||||
result = await unimplemented_action.handle("test", "<xml/>", Version.V2024_10)
|
||||
print(f"🔴 NewUnimplementedAction result: {result.xml_content}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(main())
|
||||
|
||||
@@ -4,11 +4,11 @@ import sys
|
||||
import os
|
||||
|
||||
# Add the src directory to the path so we can import our modules
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from simplified_access import (
|
||||
CustomerData,
|
||||
CustomerFactory,
|
||||
CustomerData,
|
||||
CustomerFactory,
|
||||
ResGuestFactory,
|
||||
HotelReservationIdData,
|
||||
HotelReservationIdFactory,
|
||||
@@ -20,7 +20,7 @@ from simplified_access import (
|
||||
NotifResGuests,
|
||||
RetrieveResGuests,
|
||||
NotifHotelReservationId,
|
||||
RetrieveHotelReservationId
|
||||
RetrieveHotelReservationId,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def sample_customer_data():
|
||||
phone_numbers=[
|
||||
("+1234567890", PhoneTechType.MOBILE),
|
||||
("+0987654321", PhoneTechType.VOICE),
|
||||
("+1111111111", None)
|
||||
("+1111111111", None),
|
||||
],
|
||||
email_address="john.doe@example.com",
|
||||
email_newsletter=True,
|
||||
@@ -46,17 +46,14 @@ def sample_customer_data():
|
||||
address_catalog=False,
|
||||
gender="Male",
|
||||
birth_date="1980-01-01",
|
||||
language="en"
|
||||
language="en",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_customer_data():
|
||||
"""Fixture providing minimal customer data (only required fields)."""
|
||||
return CustomerData(
|
||||
given_name="Jane",
|
||||
surname="Smith"
|
||||
)
|
||||
return CustomerData(given_name="Jane", surname="Smith")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -66,21 +63,19 @@ def sample_hotel_reservation_id_data():
|
||||
res_id_type="123",
|
||||
res_id_value="RESERVATION-456",
|
||||
res_id_source="HOTEL_SYSTEM",
|
||||
res_id_source_context="BOOKING_ENGINE"
|
||||
res_id_source_context="BOOKING_ENGINE",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def minimal_hotel_reservation_id_data():
|
||||
"""Fixture providing minimal hotel reservation ID data (only required fields)."""
|
||||
return HotelReservationIdData(
|
||||
res_id_type="999"
|
||||
)
|
||||
return HotelReservationIdData(res_id_type="999")
|
||||
|
||||
|
||||
class TestCustomerData:
|
||||
"""Test the CustomerData dataclass."""
|
||||
|
||||
|
||||
def test_customer_data_creation_full(self, sample_customer_data):
|
||||
"""Test creating CustomerData with all fields."""
|
||||
assert sample_customer_data.given_name == "John"
|
||||
@@ -89,7 +84,7 @@ class TestCustomerData:
|
||||
assert sample_customer_data.email_address == "john.doe@example.com"
|
||||
assert sample_customer_data.email_newsletter is True
|
||||
assert len(sample_customer_data.phone_numbers) == 3
|
||||
|
||||
|
||||
def test_customer_data_creation_minimal(self, minimal_customer_data):
|
||||
"""Test creating CustomerData with only required fields."""
|
||||
assert minimal_customer_data.given_name == "Jane"
|
||||
@@ -97,7 +92,7 @@ class TestCustomerData:
|
||||
assert minimal_customer_data.phone_numbers == []
|
||||
assert minimal_customer_data.email_address is None
|
||||
assert minimal_customer_data.address_line is None
|
||||
|
||||
|
||||
def test_phone_numbers_default_initialization(self):
|
||||
"""Test that phone_numbers gets initialized to empty list."""
|
||||
customer_data = CustomerData(given_name="Test", surname="User")
|
||||
@@ -106,54 +101,56 @@ class TestCustomerData:
|
||||
|
||||
class TestCustomerFactory:
|
||||
"""Test the CustomerFactory class."""
|
||||
|
||||
|
||||
def test_create_notif_customer_full(self, sample_customer_data):
|
||||
"""Test creating a NotifCustomer with full data."""
|
||||
customer = CustomerFactory.create_notif_customer(sample_customer_data)
|
||||
|
||||
|
||||
assert isinstance(customer, NotifCustomer)
|
||||
assert customer.person_name.given_name == "John"
|
||||
assert customer.person_name.surname == "Doe"
|
||||
assert customer.person_name.name_prefix == "Mr."
|
||||
assert customer.person_name.name_title == "Jr."
|
||||
|
||||
|
||||
# Check telephone
|
||||
assert len(customer.telephone) == 3
|
||||
assert customer.telephone[0].phone_number == "+1234567890"
|
||||
assert customer.telephone[0].phone_tech_type == "5" # MOBILE
|
||||
assert customer.telephone[1].phone_tech_type == "1" # VOICE
|
||||
assert customer.telephone[2].phone_tech_type is None
|
||||
|
||||
|
||||
# Check email
|
||||
assert customer.email.value == "john.doe@example.com"
|
||||
assert customer.email.remark == "newsletter:yes"
|
||||
|
||||
|
||||
# Check address
|
||||
assert customer.address.address_line == "123 Main Street"
|
||||
assert customer.address.city_name == "Anytown"
|
||||
assert customer.address.postal_code == "12345"
|
||||
assert customer.address.country_name.code == "US"
|
||||
assert customer.address.remark == "catalog:no"
|
||||
|
||||
|
||||
# Check other attributes
|
||||
assert customer.gender == "Male"
|
||||
assert customer.birth_date == "1980-01-01"
|
||||
assert customer.language == "en"
|
||||
|
||||
|
||||
def test_create_retrieve_customer_full(self, sample_customer_data):
|
||||
"""Test creating a RetrieveCustomer with full data."""
|
||||
customer = CustomerFactory.create_retrieve_customer(sample_customer_data)
|
||||
|
||||
|
||||
assert isinstance(customer, RetrieveCustomer)
|
||||
assert customer.person_name.given_name == "John"
|
||||
assert customer.person_name.surname == "Doe"
|
||||
# Same structure as NotifCustomer, so we don't need to test all fields again
|
||||
|
||||
|
||||
def test_create_customer_minimal(self, minimal_customer_data):
|
||||
"""Test creating customers with minimal data."""
|
||||
notif_customer = CustomerFactory.create_notif_customer(minimal_customer_data)
|
||||
retrieve_customer = CustomerFactory.create_retrieve_customer(minimal_customer_data)
|
||||
|
||||
retrieve_customer = CustomerFactory.create_retrieve_customer(
|
||||
minimal_customer_data
|
||||
)
|
||||
|
||||
for customer in [notif_customer, retrieve_customer]:
|
||||
assert customer.person_name.given_name == "Jane"
|
||||
assert customer.person_name.surname == "Smith"
|
||||
@@ -165,73 +162,97 @@ class TestCustomerFactory:
|
||||
assert customer.gender is None
|
||||
assert customer.birth_date is None
|
||||
assert customer.language is None
|
||||
|
||||
|
||||
def test_email_newsletter_options(self):
|
||||
"""Test different email newsletter options."""
|
||||
# Newsletter yes
|
||||
data_yes = CustomerData(given_name="Test", surname="User",
|
||||
email_address="test@example.com", email_newsletter=True)
|
||||
data_yes = CustomerData(
|
||||
given_name="Test",
|
||||
surname="User",
|
||||
email_address="test@example.com",
|
||||
email_newsletter=True,
|
||||
)
|
||||
customer = CustomerFactory.create_notif_customer(data_yes)
|
||||
assert customer.email.remark == "newsletter:yes"
|
||||
|
||||
|
||||
# Newsletter no
|
||||
data_no = CustomerData(given_name="Test", surname="User",
|
||||
email_address="test@example.com", email_newsletter=False)
|
||||
data_no = CustomerData(
|
||||
given_name="Test",
|
||||
surname="User",
|
||||
email_address="test@example.com",
|
||||
email_newsletter=False,
|
||||
)
|
||||
customer = CustomerFactory.create_notif_customer(data_no)
|
||||
assert customer.email.remark == "newsletter:no"
|
||||
|
||||
|
||||
# Newsletter not specified
|
||||
data_none = CustomerData(given_name="Test", surname="User",
|
||||
email_address="test@example.com", email_newsletter=None)
|
||||
data_none = CustomerData(
|
||||
given_name="Test",
|
||||
surname="User",
|
||||
email_address="test@example.com",
|
||||
email_newsletter=None,
|
||||
)
|
||||
customer = CustomerFactory.create_notif_customer(data_none)
|
||||
assert customer.email.remark is None
|
||||
|
||||
|
||||
def test_address_catalog_options(self):
|
||||
"""Test different address catalog options."""
|
||||
# Catalog no
|
||||
data_no = CustomerData(given_name="Test", surname="User",
|
||||
address_line="123 Street", address_catalog=False)
|
||||
data_no = CustomerData(
|
||||
given_name="Test",
|
||||
surname="User",
|
||||
address_line="123 Street",
|
||||
address_catalog=False,
|
||||
)
|
||||
customer = CustomerFactory.create_notif_customer(data_no)
|
||||
assert customer.address.remark == "catalog:no"
|
||||
|
||||
|
||||
# Catalog yes
|
||||
data_yes = CustomerData(given_name="Test", surname="User",
|
||||
address_line="123 Street", address_catalog=True)
|
||||
data_yes = CustomerData(
|
||||
given_name="Test",
|
||||
surname="User",
|
||||
address_line="123 Street",
|
||||
address_catalog=True,
|
||||
)
|
||||
customer = CustomerFactory.create_notif_customer(data_yes)
|
||||
assert customer.address.remark == "catalog:yes"
|
||||
|
||||
|
||||
# Catalog not specified
|
||||
data_none = CustomerData(given_name="Test", surname="User",
|
||||
address_line="123 Street", address_catalog=None)
|
||||
data_none = CustomerData(
|
||||
given_name="Test",
|
||||
surname="User",
|
||||
address_line="123 Street",
|
||||
address_catalog=None,
|
||||
)
|
||||
customer = CustomerFactory.create_notif_customer(data_none)
|
||||
assert customer.address.remark is None
|
||||
|
||||
|
||||
def test_from_notif_customer_roundtrip(self, sample_customer_data):
|
||||
"""Test converting NotifCustomer back to CustomerData."""
|
||||
customer = CustomerFactory.create_notif_customer(sample_customer_data)
|
||||
converted_data = CustomerFactory.from_notif_customer(customer)
|
||||
|
||||
|
||||
assert converted_data == sample_customer_data
|
||||
|
||||
|
||||
def test_from_retrieve_customer_roundtrip(self, sample_customer_data):
|
||||
"""Test converting RetrieveCustomer back to CustomerData."""
|
||||
customer = CustomerFactory.create_retrieve_customer(sample_customer_data)
|
||||
converted_data = CustomerFactory.from_retrieve_customer(customer)
|
||||
|
||||
|
||||
assert converted_data == sample_customer_data
|
||||
|
||||
|
||||
def test_phone_tech_type_conversion(self):
|
||||
"""Test that PhoneTechType enum values are properly converted."""
|
||||
data = CustomerData(
|
||||
given_name="Test",
|
||||
given_name="Test",
|
||||
surname="User",
|
||||
phone_numbers=[
|
||||
("+1111111111", PhoneTechType.VOICE),
|
||||
("+2222222222", PhoneTechType.FAX),
|
||||
("+3333333333", PhoneTechType.MOBILE)
|
||||
]
|
||||
("+3333333333", PhoneTechType.MOBILE),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
customer = CustomerFactory.create_notif_customer(data)
|
||||
assert customer.telephone[0].phone_tech_type == "1" # VOICE
|
||||
assert customer.telephone[1].phone_tech_type == "3" # FAX
|
||||
@@ -240,15 +261,21 @@ class TestCustomerFactory:
|
||||
|
||||
class TestHotelReservationIdData:
|
||||
"""Test the HotelReservationIdData dataclass."""
|
||||
|
||||
def test_hotel_reservation_id_data_creation_full(self, sample_hotel_reservation_id_data):
|
||||
|
||||
def test_hotel_reservation_id_data_creation_full(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test creating HotelReservationIdData with all fields."""
|
||||
assert sample_hotel_reservation_id_data.res_id_type == "123"
|
||||
assert sample_hotel_reservation_id_data.res_id_value == "RESERVATION-456"
|
||||
assert sample_hotel_reservation_id_data.res_id_source == "HOTEL_SYSTEM"
|
||||
assert sample_hotel_reservation_id_data.res_id_source_context == "BOOKING_ENGINE"
|
||||
|
||||
def test_hotel_reservation_id_data_creation_minimal(self, minimal_hotel_reservation_id_data):
|
||||
assert (
|
||||
sample_hotel_reservation_id_data.res_id_source_context == "BOOKING_ENGINE"
|
||||
)
|
||||
|
||||
def test_hotel_reservation_id_data_creation_minimal(
|
||||
self, minimal_hotel_reservation_id_data
|
||||
):
|
||||
"""Test creating HotelReservationIdData with only required fields."""
|
||||
assert minimal_hotel_reservation_id_data.res_id_type == "999"
|
||||
assert minimal_hotel_reservation_id_data.res_id_value is None
|
||||
@@ -258,124 +285,158 @@ class TestHotelReservationIdData:
|
||||
|
||||
class TestHotelReservationIdFactory:
|
||||
"""Test the HotelReservationIdFactory class."""
|
||||
|
||||
def test_create_notif_hotel_reservation_id_full(self, sample_hotel_reservation_id_data):
|
||||
|
||||
def test_create_notif_hotel_reservation_id_full(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test creating a NotifHotelReservationId with full data."""
|
||||
reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||
|
||||
reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(
|
||||
sample_hotel_reservation_id_data
|
||||
)
|
||||
|
||||
assert isinstance(reservation_id, NotifHotelReservationId)
|
||||
assert reservation_id.res_id_type == "123"
|
||||
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||
assert reservation_id.res_id_source == "HOTEL_SYSTEM"
|
||||
assert reservation_id.res_id_source_context == "BOOKING_ENGINE"
|
||||
|
||||
def test_create_retrieve_hotel_reservation_id_full(self, sample_hotel_reservation_id_data):
|
||||
|
||||
def test_create_retrieve_hotel_reservation_id_full(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test creating a RetrieveHotelReservationId with full data."""
|
||||
reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||
|
||||
reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(
|
||||
sample_hotel_reservation_id_data
|
||||
)
|
||||
|
||||
assert isinstance(reservation_id, RetrieveHotelReservationId)
|
||||
assert reservation_id.res_id_type == "123"
|
||||
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||
assert reservation_id.res_id_source == "HOTEL_SYSTEM"
|
||||
assert reservation_id.res_id_source_context == "BOOKING_ENGINE"
|
||||
|
||||
def test_create_hotel_reservation_id_minimal(self, minimal_hotel_reservation_id_data):
|
||||
|
||||
def test_create_hotel_reservation_id_minimal(
|
||||
self, minimal_hotel_reservation_id_data
|
||||
):
|
||||
"""Test creating hotel reservation IDs with minimal data."""
|
||||
notif_reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(minimal_hotel_reservation_id_data)
|
||||
retrieve_reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(minimal_hotel_reservation_id_data)
|
||||
|
||||
notif_reservation_id = (
|
||||
HotelReservationIdFactory.create_notif_hotel_reservation_id(
|
||||
minimal_hotel_reservation_id_data
|
||||
)
|
||||
)
|
||||
retrieve_reservation_id = (
|
||||
HotelReservationIdFactory.create_retrieve_hotel_reservation_id(
|
||||
minimal_hotel_reservation_id_data
|
||||
)
|
||||
)
|
||||
|
||||
for reservation_id in [notif_reservation_id, retrieve_reservation_id]:
|
||||
assert reservation_id.res_id_type == "999"
|
||||
assert reservation_id.res_id_value is None
|
||||
assert reservation_id.res_id_source is None
|
||||
assert reservation_id.res_id_source_context is None
|
||||
|
||||
def test_from_notif_hotel_reservation_id_roundtrip(self, sample_hotel_reservation_id_data):
|
||||
|
||||
def test_from_notif_hotel_reservation_id_roundtrip(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test converting NotifHotelReservationId back to HotelReservationIdData."""
|
||||
reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||
converted_data = HotelReservationIdFactory.from_notif_hotel_reservation_id(reservation_id)
|
||||
|
||||
reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(
|
||||
sample_hotel_reservation_id_data
|
||||
)
|
||||
converted_data = HotelReservationIdFactory.from_notif_hotel_reservation_id(
|
||||
reservation_id
|
||||
)
|
||||
|
||||
assert converted_data == sample_hotel_reservation_id_data
|
||||
|
||||
def test_from_retrieve_hotel_reservation_id_roundtrip(self, sample_hotel_reservation_id_data):
|
||||
|
||||
def test_from_retrieve_hotel_reservation_id_roundtrip(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test converting RetrieveHotelReservationId back to HotelReservationIdData."""
|
||||
reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||
converted_data = HotelReservationIdFactory.from_retrieve_hotel_reservation_id(reservation_id)
|
||||
|
||||
reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(
|
||||
sample_hotel_reservation_id_data
|
||||
)
|
||||
converted_data = HotelReservationIdFactory.from_retrieve_hotel_reservation_id(
|
||||
reservation_id
|
||||
)
|
||||
|
||||
assert converted_data == sample_hotel_reservation_id_data
|
||||
|
||||
|
||||
class TestResGuestFactory:
|
||||
"""Test the ResGuestFactory class."""
|
||||
|
||||
|
||||
def test_create_notif_res_guests(self, sample_customer_data):
|
||||
"""Test creating NotifResGuests structure."""
|
||||
res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||
|
||||
|
||||
assert isinstance(res_guests, NotifResGuests)
|
||||
|
||||
|
||||
# Navigate down the nested structure
|
||||
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
assert customer.person_name.given_name == "John"
|
||||
assert customer.person_name.surname == "Doe"
|
||||
assert customer.email.value == "john.doe@example.com"
|
||||
|
||||
|
||||
def test_create_retrieve_res_guests(self, sample_customer_data):
|
||||
"""Test creating RetrieveResGuests structure."""
|
||||
res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||
|
||||
|
||||
assert isinstance(res_guests, RetrieveResGuests)
|
||||
|
||||
|
||||
# Navigate down the nested structure
|
||||
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
assert customer.person_name.given_name == "John"
|
||||
assert customer.person_name.surname == "Doe"
|
||||
assert customer.email.value == "john.doe@example.com"
|
||||
|
||||
|
||||
def test_create_res_guests_minimal(self, minimal_customer_data):
|
||||
"""Test creating ResGuests with minimal customer data."""
|
||||
notif_res_guests = ResGuestFactory.create_notif_res_guests(minimal_customer_data)
|
||||
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(minimal_customer_data)
|
||||
|
||||
notif_res_guests = ResGuestFactory.create_notif_res_guests(
|
||||
minimal_customer_data
|
||||
)
|
||||
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(
|
||||
minimal_customer_data
|
||||
)
|
||||
|
||||
for res_guests in [notif_res_guests, retrieve_res_guests]:
|
||||
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
assert customer.person_name.given_name == "Jane"
|
||||
assert customer.person_name.surname == "Smith"
|
||||
assert customer.email is None
|
||||
assert customer.address is None
|
||||
|
||||
|
||||
def test_extract_primary_customer_notif(self, sample_customer_data):
|
||||
"""Test extracting primary customer from NotifResGuests."""
|
||||
res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||
|
||||
|
||||
assert extracted_data == sample_customer_data
|
||||
|
||||
|
||||
def test_extract_primary_customer_retrieve(self, sample_customer_data):
|
||||
"""Test extracting primary customer from RetrieveResGuests."""
|
||||
res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||
|
||||
|
||||
assert extracted_data == sample_customer_data
|
||||
|
||||
|
||||
def test_roundtrip_conversion_notif(self, sample_customer_data):
|
||||
"""Test complete roundtrip: CustomerData -> NotifResGuests -> CustomerData."""
|
||||
res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||
|
||||
|
||||
assert extracted_data == sample_customer_data
|
||||
|
||||
|
||||
def test_roundtrip_conversion_retrieve(self, sample_customer_data):
|
||||
"""Test complete roundtrip: CustomerData -> RetrieveResGuests -> CustomerData."""
|
||||
res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||
extracted_data = ResGuestFactory.extract_primary_customer(res_guests)
|
||||
|
||||
|
||||
assert extracted_data == sample_customer_data
|
||||
|
||||
|
||||
class TestPhoneTechType:
|
||||
"""Test the PhoneTechType enum."""
|
||||
|
||||
|
||||
def test_enum_values(self):
|
||||
"""Test that enum values are correct."""
|
||||
assert PhoneTechType.VOICE.value == "1"
|
||||
@@ -385,95 +446,121 @@ class TestPhoneTechType:
|
||||
|
||||
class TestAlpineBitsFactory:
|
||||
"""Test the unified AlpineBitsFactory class."""
|
||||
|
||||
|
||||
def test_create_customer_notif(self, sample_customer_data):
|
||||
"""Test creating customer using unified factory for NOTIF."""
|
||||
customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.NOTIF)
|
||||
assert isinstance(customer, NotifCustomer)
|
||||
assert customer.person_name.given_name == "John"
|
||||
assert customer.person_name.surname == "Doe"
|
||||
|
||||
|
||||
def test_create_customer_retrieve(self, sample_customer_data):
|
||||
"""Test creating customer using unified factory for RETRIEVE."""
|
||||
customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||
customer = AlpineBitsFactory.create(
|
||||
sample_customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
assert isinstance(customer, RetrieveCustomer)
|
||||
assert customer.person_name.given_name == "John"
|
||||
assert customer.person_name.surname == "Doe"
|
||||
|
||||
|
||||
def test_create_hotel_reservation_id_notif(self, sample_hotel_reservation_id_data):
|
||||
"""Test creating hotel reservation ID using unified factory for NOTIF."""
|
||||
reservation_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.NOTIF)
|
||||
reservation_id = AlpineBitsFactory.create(
|
||||
sample_hotel_reservation_id_data, OtaMessageType.NOTIF
|
||||
)
|
||||
assert isinstance(reservation_id, NotifHotelReservationId)
|
||||
assert reservation_id.res_id_type == "123"
|
||||
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||
|
||||
def test_create_hotel_reservation_id_retrieve(self, sample_hotel_reservation_id_data):
|
||||
|
||||
def test_create_hotel_reservation_id_retrieve(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test creating hotel reservation ID using unified factory for RETRIEVE."""
|
||||
reservation_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.RETRIEVE)
|
||||
reservation_id = AlpineBitsFactory.create(
|
||||
sample_hotel_reservation_id_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
assert isinstance(reservation_id, RetrieveHotelReservationId)
|
||||
assert reservation_id.res_id_type == "123"
|
||||
assert reservation_id.res_id_value == "RESERVATION-456"
|
||||
|
||||
|
||||
def test_create_res_guests_notif(self, sample_customer_data):
|
||||
"""Test creating ResGuests using unified factory for NOTIF."""
|
||||
res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.NOTIF)
|
||||
res_guests = AlpineBitsFactory.create_res_guests(
|
||||
sample_customer_data, OtaMessageType.NOTIF
|
||||
)
|
||||
assert isinstance(res_guests, NotifResGuests)
|
||||
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
assert customer.person_name.given_name == "John"
|
||||
|
||||
|
||||
def test_create_res_guests_retrieve(self, sample_customer_data):
|
||||
"""Test creating ResGuests using unified factory for RETRIEVE."""
|
||||
res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||
res_guests = AlpineBitsFactory.create_res_guests(
|
||||
sample_customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
assert isinstance(res_guests, RetrieveResGuests)
|
||||
customer = res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
assert customer.person_name.given_name == "John"
|
||||
|
||||
|
||||
def test_extract_data_from_customer(self, sample_customer_data):
|
||||
"""Test extracting data from customer objects."""
|
||||
# Create both types and extract data back
|
||||
notif_customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.NOTIF)
|
||||
retrieve_customer = AlpineBitsFactory.create(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
notif_customer = AlpineBitsFactory.create(
|
||||
sample_customer_data, OtaMessageType.NOTIF
|
||||
)
|
||||
retrieve_customer = AlpineBitsFactory.create(
|
||||
sample_customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
notif_extracted = AlpineBitsFactory.extract_data(notif_customer)
|
||||
retrieve_extracted = AlpineBitsFactory.extract_data(retrieve_customer)
|
||||
|
||||
|
||||
assert notif_extracted == sample_customer_data
|
||||
assert retrieve_extracted == sample_customer_data
|
||||
|
||||
def test_extract_data_from_hotel_reservation_id(self, sample_hotel_reservation_id_data):
|
||||
|
||||
def test_extract_data_from_hotel_reservation_id(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test extracting data from hotel reservation ID objects."""
|
||||
# Create both types and extract data back
|
||||
notif_res_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.NOTIF)
|
||||
retrieve_res_id = AlpineBitsFactory.create(sample_hotel_reservation_id_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
notif_res_id = AlpineBitsFactory.create(
|
||||
sample_hotel_reservation_id_data, OtaMessageType.NOTIF
|
||||
)
|
||||
retrieve_res_id = AlpineBitsFactory.create(
|
||||
sample_hotel_reservation_id_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
notif_extracted = AlpineBitsFactory.extract_data(notif_res_id)
|
||||
retrieve_extracted = AlpineBitsFactory.extract_data(retrieve_res_id)
|
||||
|
||||
|
||||
assert notif_extracted == sample_hotel_reservation_id_data
|
||||
assert retrieve_extracted == sample_hotel_reservation_id_data
|
||||
|
||||
|
||||
def test_extract_data_from_res_guests(self, sample_customer_data):
|
||||
"""Test extracting data from ResGuests objects."""
|
||||
# Create both types and extract data back
|
||||
notif_res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.NOTIF)
|
||||
retrieve_res_guests = AlpineBitsFactory.create_res_guests(sample_customer_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
notif_res_guests = AlpineBitsFactory.create_res_guests(
|
||||
sample_customer_data, OtaMessageType.NOTIF
|
||||
)
|
||||
retrieve_res_guests = AlpineBitsFactory.create_res_guests(
|
||||
sample_customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
notif_extracted = AlpineBitsFactory.extract_data(notif_res_guests)
|
||||
retrieve_extracted = AlpineBitsFactory.extract_data(retrieve_res_guests)
|
||||
|
||||
|
||||
assert notif_extracted == sample_customer_data
|
||||
assert retrieve_extracted == sample_customer_data
|
||||
|
||||
|
||||
def test_unsupported_data_type_error(self):
|
||||
"""Test that unsupported data types raise ValueError."""
|
||||
with pytest.raises(ValueError, match="Unsupported data type"):
|
||||
AlpineBitsFactory.create("invalid_data", OtaMessageType.NOTIF)
|
||||
|
||||
|
||||
def test_unsupported_object_type_error(self):
|
||||
"""Test that unsupported object types raise ValueError in extract_data."""
|
||||
with pytest.raises(ValueError, match="Unsupported object type"):
|
||||
AlpineBitsFactory.extract_data("invalid_object")
|
||||
|
||||
|
||||
def test_complete_workflow_with_unified_factory(self):
|
||||
"""Test a complete workflow using only the unified factory."""
|
||||
# Original data
|
||||
@@ -481,34 +568,47 @@ class TestAlpineBitsFactory:
|
||||
given_name="Unified",
|
||||
surname="Factory",
|
||||
email_address="unified@factory.com",
|
||||
phone_numbers=[("+1234567890", PhoneTechType.MOBILE)]
|
||||
phone_numbers=[("+1234567890", PhoneTechType.MOBILE)],
|
||||
)
|
||||
|
||||
|
||||
reservation_data = HotelReservationIdData(
|
||||
res_id_type="999",
|
||||
res_id_value="UNIFIED-TEST"
|
||||
res_id_type="999", res_id_value="UNIFIED-TEST"
|
||||
)
|
||||
|
||||
|
||||
# Create using unified factory
|
||||
customer_notif = AlpineBitsFactory.create(customer_data, OtaMessageType.NOTIF)
|
||||
customer_retrieve = AlpineBitsFactory.create(customer_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
customer_retrieve = AlpineBitsFactory.create(
|
||||
customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
res_id_notif = AlpineBitsFactory.create(reservation_data, OtaMessageType.NOTIF)
|
||||
res_id_retrieve = AlpineBitsFactory.create(reservation_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
res_guests_notif = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.NOTIF)
|
||||
res_guests_retrieve = AlpineBitsFactory.create_res_guests(customer_data, OtaMessageType.RETRIEVE)
|
||||
|
||||
res_id_retrieve = AlpineBitsFactory.create(
|
||||
reservation_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
res_guests_notif = AlpineBitsFactory.create_res_guests(
|
||||
customer_data, OtaMessageType.NOTIF
|
||||
)
|
||||
res_guests_retrieve = AlpineBitsFactory.create_res_guests(
|
||||
customer_data, OtaMessageType.RETRIEVE
|
||||
)
|
||||
|
||||
# Extract everything back
|
||||
extracted_customer_from_notif = AlpineBitsFactory.extract_data(customer_notif)
|
||||
extracted_customer_from_retrieve = AlpineBitsFactory.extract_data(customer_retrieve)
|
||||
|
||||
extracted_customer_from_retrieve = AlpineBitsFactory.extract_data(
|
||||
customer_retrieve
|
||||
)
|
||||
|
||||
extracted_res_id_from_notif = AlpineBitsFactory.extract_data(res_id_notif)
|
||||
extracted_res_id_from_retrieve = AlpineBitsFactory.extract_data(res_id_retrieve)
|
||||
|
||||
extracted_from_res_guests_notif = AlpineBitsFactory.extract_data(res_guests_notif)
|
||||
extracted_from_res_guests_retrieve = AlpineBitsFactory.extract_data(res_guests_retrieve)
|
||||
|
||||
|
||||
extracted_from_res_guests_notif = AlpineBitsFactory.extract_data(
|
||||
res_guests_notif
|
||||
)
|
||||
extracted_from_res_guests_retrieve = AlpineBitsFactory.extract_data(
|
||||
res_guests_retrieve
|
||||
)
|
||||
|
||||
# Verify everything matches
|
||||
assert extracted_customer_from_notif == customer_data
|
||||
assert extracted_customer_from_retrieve == customer_data
|
||||
@@ -520,37 +620,72 @@ class TestAlpineBitsFactory:
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests combining both factories."""
|
||||
|
||||
|
||||
def test_both_factories_produce_same_customer_data(self, sample_customer_data):
|
||||
"""Test that both factories can work with the same customer data."""
|
||||
# Create using CustomerFactory
|
||||
notif_customer = CustomerFactory.create_notif_customer(sample_customer_data)
|
||||
retrieve_customer = CustomerFactory.create_retrieve_customer(sample_customer_data)
|
||||
|
||||
retrieve_customer = CustomerFactory.create_retrieve_customer(
|
||||
sample_customer_data
|
||||
)
|
||||
|
||||
# Create using ResGuestFactory and extract customers
|
||||
notif_res_guests = ResGuestFactory.create_notif_res_guests(sample_customer_data)
|
||||
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(sample_customer_data)
|
||||
|
||||
notif_from_res_guests = notif_res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
retrieve_from_res_guests = retrieve_res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
|
||||
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(
|
||||
sample_customer_data
|
||||
)
|
||||
|
||||
notif_from_res_guests = (
|
||||
notif_res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
)
|
||||
retrieve_from_res_guests = (
|
||||
retrieve_res_guests.res_guest.profiles.profile_info.profile.customer
|
||||
)
|
||||
|
||||
# Compare customer names (structure should be identical)
|
||||
assert notif_customer.person_name.given_name == notif_from_res_guests.person_name.given_name
|
||||
assert notif_customer.person_name.surname == notif_from_res_guests.person_name.surname
|
||||
assert retrieve_customer.person_name.given_name == retrieve_from_res_guests.person_name.given_name
|
||||
assert retrieve_customer.person_name.surname == retrieve_from_res_guests.person_name.surname
|
||||
|
||||
def test_hotel_reservation_id_factories_produce_same_data(self, sample_hotel_reservation_id_data):
|
||||
assert (
|
||||
notif_customer.person_name.given_name
|
||||
== notif_from_res_guests.person_name.given_name
|
||||
)
|
||||
assert (
|
||||
notif_customer.person_name.surname
|
||||
== notif_from_res_guests.person_name.surname
|
||||
)
|
||||
assert (
|
||||
retrieve_customer.person_name.given_name
|
||||
== retrieve_from_res_guests.person_name.given_name
|
||||
)
|
||||
assert (
|
||||
retrieve_customer.person_name.surname
|
||||
== retrieve_from_res_guests.person_name.surname
|
||||
)
|
||||
|
||||
def test_hotel_reservation_id_factories_produce_same_data(
|
||||
self, sample_hotel_reservation_id_data
|
||||
):
|
||||
"""Test that both HotelReservationId factories produce equivalent results."""
|
||||
notif_reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||
retrieve_reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(sample_hotel_reservation_id_data)
|
||||
|
||||
notif_reservation_id = (
|
||||
HotelReservationIdFactory.create_notif_hotel_reservation_id(
|
||||
sample_hotel_reservation_id_data
|
||||
)
|
||||
)
|
||||
retrieve_reservation_id = (
|
||||
HotelReservationIdFactory.create_retrieve_hotel_reservation_id(
|
||||
sample_hotel_reservation_id_data
|
||||
)
|
||||
)
|
||||
|
||||
# Both should have the same field values
|
||||
assert notif_reservation_id.res_id_type == retrieve_reservation_id.res_id_type
|
||||
assert notif_reservation_id.res_id_value == retrieve_reservation_id.res_id_value
|
||||
assert notif_reservation_id.res_id_source == retrieve_reservation_id.res_id_source
|
||||
assert notif_reservation_id.res_id_source_context == retrieve_reservation_id.res_id_source_context
|
||||
|
||||
assert (
|
||||
notif_reservation_id.res_id_source == retrieve_reservation_id.res_id_source
|
||||
)
|
||||
assert (
|
||||
notif_reservation_id.res_id_source_context
|
||||
== retrieve_reservation_id.res_id_source_context
|
||||
)
|
||||
|
||||
def test_complex_customer_workflow(self):
|
||||
"""Test a complex workflow with multiple operations."""
|
||||
# Create original data
|
||||
@@ -559,7 +694,7 @@ class TestIntegration:
|
||||
surname="Johnson",
|
||||
phone_numbers=[
|
||||
("+1555123456", PhoneTechType.MOBILE),
|
||||
("+1555654321", PhoneTechType.VOICE)
|
||||
("+1555654321", PhoneTechType.VOICE),
|
||||
],
|
||||
email_address="alice.johnson@company.com",
|
||||
email_newsletter=False,
|
||||
@@ -569,22 +704,24 @@ class TestIntegration:
|
||||
country_code="CA",
|
||||
address_catalog=True,
|
||||
gender="Female",
|
||||
language="fr"
|
||||
language="fr",
|
||||
)
|
||||
|
||||
|
||||
# Create ResGuests for both types
|
||||
notif_res_guests = ResGuestFactory.create_notif_res_guests(original_data)
|
||||
retrieve_res_guests = ResGuestFactory.create_retrieve_res_guests(original_data)
|
||||
|
||||
|
||||
# Extract data back from both
|
||||
notif_extracted = ResGuestFactory.extract_primary_customer(notif_res_guests)
|
||||
retrieve_extracted = ResGuestFactory.extract_primary_customer(retrieve_res_guests)
|
||||
|
||||
retrieve_extracted = ResGuestFactory.extract_primary_customer(
|
||||
retrieve_res_guests
|
||||
)
|
||||
|
||||
# All should be equal
|
||||
assert original_data == notif_extracted
|
||||
assert original_data == retrieve_extracted
|
||||
assert notif_extracted == retrieve_extracted
|
||||
|
||||
|
||||
def test_complex_hotel_reservation_id_workflow(self):
|
||||
"""Test a complex workflow with HotelReservationId operations."""
|
||||
# Create original reservation ID data
|
||||
@@ -592,18 +729,30 @@ class TestIntegration:
|
||||
res_id_type="456",
|
||||
res_id_value="COMPLEX-RESERVATION-789",
|
||||
res_id_source="INTEGRATION_SYSTEM",
|
||||
res_id_source_context="API_CALL"
|
||||
res_id_source_context="API_CALL",
|
||||
)
|
||||
|
||||
|
||||
# Create HotelReservationId for both types
|
||||
notif_reservation_id = HotelReservationIdFactory.create_notif_hotel_reservation_id(original_data)
|
||||
retrieve_reservation_id = HotelReservationIdFactory.create_retrieve_hotel_reservation_id(original_data)
|
||||
|
||||
notif_reservation_id = (
|
||||
HotelReservationIdFactory.create_notif_hotel_reservation_id(original_data)
|
||||
)
|
||||
retrieve_reservation_id = (
|
||||
HotelReservationIdFactory.create_retrieve_hotel_reservation_id(
|
||||
original_data
|
||||
)
|
||||
)
|
||||
|
||||
# Extract data back from both
|
||||
notif_extracted = HotelReservationIdFactory.from_notif_hotel_reservation_id(notif_reservation_id)
|
||||
retrieve_extracted = HotelReservationIdFactory.from_retrieve_hotel_reservation_id(retrieve_reservation_id)
|
||||
|
||||
notif_extracted = HotelReservationIdFactory.from_notif_hotel_reservation_id(
|
||||
notif_reservation_id
|
||||
)
|
||||
retrieve_extracted = (
|
||||
HotelReservationIdFactory.from_retrieve_hotel_reservation_id(
|
||||
retrieve_reservation_id
|
||||
)
|
||||
)
|
||||
|
||||
# All should be equal
|
||||
assert original_data == notif_extracted
|
||||
assert original_data == retrieve_extracted
|
||||
assert notif_extracted == retrieve_extracted
|
||||
assert notif_extracted == retrieve_extracted
|
||||
|
||||
@@ -6,24 +6,31 @@ Test the handshake functionality with the real AlpineBits sample file.
|
||||
import asyncio
|
||||
from alpine_bits_python.alpinebits_server import AlpineBitsServer
|
||||
|
||||
|
||||
async def main():
|
||||
print("🔄 Testing AlpineBits Handshake with Sample File")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
# Create server instance
|
||||
server = AlpineBitsServer()
|
||||
|
||||
|
||||
# Read the sample handshake request
|
||||
with open("AlpineBits-HotelData-2024-10/files/samples/Handshake/Handshake-OTA_PingRQ.xml", "r") as f:
|
||||
with open(
|
||||
"AlpineBits-HotelData-2024-10/files/samples/Handshake/Handshake-OTA_PingRQ.xml",
|
||||
"r",
|
||||
) as f:
|
||||
ping_request_xml = f.read()
|
||||
|
||||
|
||||
print("📤 Sending handshake request...")
|
||||
|
||||
|
||||
# Handle the ping request
|
||||
response = await server.handle_request("OTA_Ping:Handshaking", ping_request_xml, "2024-10")
|
||||
|
||||
response = await server.handle_request(
|
||||
"OTA_Ping:Handshaking", ping_request_xml, "2024-10"
|
||||
)
|
||||
|
||||
print(f"\n📥 Response Status: {response.status_code}")
|
||||
print(f"📄 Response XML:\n{response.xml_content}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
asyncio.run(main())
|
||||
|
||||
711
uv.lock
generated
711
uv.lock
generated
@@ -1,26 +1,56 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alpine-bits-python-server"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "annotatedyaml" },
|
||||
{ name = "dotenv" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "generateds" },
|
||||
{ name = "lxml" },
|
||||
{ name = "pytest" },
|
||||
{ name = "redis" },
|
||||
{ name = "ruff" },
|
||||
{ name = "slowapi" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "voluptuous" },
|
||||
{ name = "xsdata", extra = ["cli", "lxml", "soap"] },
|
||||
{ name = "xsdata-pydantic", extra = ["cli", "lxml", "soap"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiosqlite", specifier = ">=0.21.0" },
|
||||
{ name = "annotatedyaml", specifier = ">=1.0.0" },
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "fastapi", specifier = ">=0.117.1" },
|
||||
{ name = "generateds", specifier = ">=2.44.3" },
|
||||
{ name = "lxml", specifier = ">=6.0.1" },
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
{ name = "redis", specifier = ">=6.4.0" },
|
||||
{ name = "ruff", specifier = ">=0.13.1" },
|
||||
{ name = "slowapi", specifier = ">=0.1.9" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.43" },
|
||||
{ name = "uvicorn", specifier = ">=0.37.0" },
|
||||
{ name = "voluptuous", specifier = ">=0.15.2" },
|
||||
{ name = "xsdata", extras = ["cli", "lxml", "soap"], specifier = ">=25.7" },
|
||||
{ name = "xsdata-pydantic", extras = ["cli", "lxml", "soap"], specifier = ">=24.5" },
|
||||
]
|
||||
@@ -29,49 +59,102 @@ requires-dist = [
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotatedyaml"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "propcache" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "voluptuous" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/a7/fe560a0e2395a69415be19534451a78820117a7402e0d4a4b19311b63938/annotatedyaml-1.0.0.tar.gz", hash = "sha256:14ca00ee8e8fa0519a24059e38de4ef7e58358abf21fb5755d5f4d3ab9717ae6", size = 15346, upload-time = "2025-08-01T19:18:26.365Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/2a/f202b7449e57e31bf3df3cb91d52ca42d4619f162100271321ee10e0d1d9/annotatedyaml-1.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd22387e4944ad61af32d30789979e697de70d9cd51c6ba98bf7519290e06dbf", size = 58716, upload-time = "2025-08-01T19:22:35.477Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/d8/60df39db11a47644fe430cdd65bfe83b8733e41be8604a275d374ee6ba89/annotatedyaml-1.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b5134aee0cd807a0355a6bc97a383943e2498a839e039ec214cb493ed249327", size = 58386, upload-time = "2025-08-01T19:22:37.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e5/4650b5684427aa88fdbdfdbba33c3a5ef27efc4594155f7414827663296b/annotatedyaml-1.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:638a7d8653fa4c2f6adfc5608edd7943a8b398b8575752cee36a1f002fc9a4a1", size = 70500, upload-time = "2025-08-01T19:22:38.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/66/330b76ad43ceccf2d897ca14b65713b60038282c66cbf13a9a116487a1e5/annotatedyaml-1.0.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:019751d57ecb3fc975960a4ff432bfff602caea4875ce5af1d8996c66d9af7c3", size = 64814, upload-time = "2025-08-01T19:22:39.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/30/6b9888e3dc7fb0dba9af4aa6f660c55121b2c927dccb5f7821c5124de907/annotatedyaml-1.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:690156bd1a8ac0dee12096cf933ee13a7b3029ecc17a06eddbdc38e96bccc67d", size = 71188, upload-time = "2025-08-01T19:22:40.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/b0/998ebb6bf773dff32e6e7465025829f1f8564a19c9dcd63550e6878891a0/annotatedyaml-1.0.0-cp313-cp313-manylinux_2_36_x86_64.whl", hash = "sha256:b4ce6ea9fe5690a53125d28ef8730ad6cad914eec82ebd598bc68b0a26ffe055", size = 69428, upload-time = "2025-08-01T19:18:24.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/16/88d507292deb34761435188c896dd56ccca4ad3f40dda6473e55b86d94ac/annotatedyaml-1.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8c08f5a8adefd8032a0dc19980fefec9531f9b44e39b2ca25c27bd511db7b70c", size = 71142, upload-time = "2025-08-01T19:22:41.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3f/cae0e2ba5cacfc7937b4c6661864c69a3dec3ddcec7e48a952aa5321401f/annotatedyaml-1.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7ae86ba9bb1c14b47376171be06ea920b5f99fa794dc783edde9158b32dfdaa2", size = 65678, upload-time = "2025-08-01T19:22:42.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/35/94b0575e7f94536f3f368238bc08d6a99df4d813cf6a960bd750e0dc669e/annotatedyaml-1.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65438a2542bac3441e3f2be1578126fcb69de16031f27d65a493072b4f50012f", size = 71966, upload-time = "2025-08-01T19:22:44.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/da/36998f0b3a2e8be96a1e0a69a8a16952fcd13fed97e31210c790624210c5/annotatedyaml-1.0.0-cp313-cp313-win32.whl", hash = "sha256:4a39e4f2ecfb931a5d4143f164ded955f673a8e3e0414d8b7dea50cb79739665", size = 55949, upload-time = "2025-08-01T19:22:47.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/04/1ea5526e02f29c82b1e8a25f3a502c89fd1de93a24f89e6eb501f001673f/annotatedyaml-1.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8969e9c9b92c1ee578615200580b0955ff683a1f7990fb1334872340dbcb3fd5", size = 60181, upload-time = "2025-08-01T19:22:48.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/0f/1ea98daa17aa24feda1d4a1434bca5158d120519e79f7ae9063e58adf8cf/annotatedyaml-1.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5895b2292dd141cf054653249ec8d0fdff6dfdeec894366378ecf7634fc90b2e", size = 58746, upload-time = "2025-08-01T19:22:49.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/b7/0a90df653c006022a7a5c382699159751cd9d8c3867913a3f1151d87be8f/annotatedyaml-1.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:22f8c38ee8f784dd5ee376a5d7dadec0bf76d8f5c366231df85e645bf2608f74", size = 58621, upload-time = "2025-08-01T19:22:51.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/61/381124e728254827f0ab70f86cea31416fd299372e9777b735c4e7ba0954/annotatedyaml-1.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24136eb4868f1233d6e46728f0ff7028dd13038655105f7919242879231499a4", size = 71020, upload-time = "2025-08-01T19:22:52.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/d4/cf089b9a83ad52359968e6ebf2edca61decbc9bcb9fa344f61b6ea655d67/annotatedyaml-1.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae3670c327c1b19906757d7c9b78e87930fe463f2323cabc1994ae0d1badc8d8", size = 70998, upload-time = "2025-08-01T19:22:53.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/28/c1ce664bd094ded592b4b6fd04e2e79f64b5598ab2065bc578aad6f5255d/annotatedyaml-1.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7457fed9c2dad416309a4e4f9aacaacdab70d6492a892f37c23b0acb8bb31f09", size = 71632, upload-time = "2025-08-01T19:22:54.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/43/9c92718518f66a4f0d3a1d3a76f886fa881e365098d520c7a3f39d1652d6/annotatedyaml-1.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae74effa8bc1c3f2a1195f1d186a41806d97fd6b7f81c05be38393b67dbbfbdb", size = 72150, upload-time = "2025-08-01T19:22:55.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/36/306ac9f5e94871405d0f256d8e835b05cd20aeee2b68e33dfde1b02ed7aa/annotatedyaml-1.0.0-cp314-cp314-win32.whl", hash = "sha256:89ea081d55a3fe291a5a131f859a2dd9c2a7fb991da10785a3f24743a91403a9", size = 57125, upload-time = "2025-08-01T19:22:56.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/94/9a88a7a77a8324de818991a31ed56b0b046761f8f3d222da80f5565e6945/annotatedyaml-1.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:427fb462c6d6326f355c71fd90362adcfa58ddf5d8d4482fb0d2b691afec11ea", size = 61262, upload-time = "2025-08-01T19:22:57.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/1b/a64ec83b1ea854bb79481ddd4af4c4cfd9d021d3f5817b1863cb4b1b83d0/annotatedyaml-1.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:bf22881a3bea4daa69eee99fc3aa5b5a7110b1a514533b6256559512aea9b459", size = 104485, upload-time = "2025-08-01T19:22:58.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/24/b0842661f87c52a5833a345ec334075b503a23253ae73eba1cd410852f0f/annotatedyaml-1.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b08df8682dcf4f3aea813b42074032c4621330373ee2c2fec2f967a9f38d6921", size = 104355, upload-time = "2025-08-01T19:22:59.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d2/674bcb54aa8a5d9075689a4124d5e8700799c6822874861689cc49012ee3/annotatedyaml-1.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e4d8389647936bb49e66884b3a91e291bbf3c34080d1c0f59fc8e2601ba0420", size = 130215, upload-time = "2025-08-01T19:23:01.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/93/ff4aadf967e9efbab7b021b1ce2e5c67f7adaff8551b5591747013c3ecf4/annotatedyaml-1.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c5312eafe6ec7a61f9e29e71edd577a844bffee4b64a15e2550df38a0ea9995", size = 128836, upload-time = "2025-08-01T19:23:03.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/0a/f04dee491f3736d3e92a42faf5b821c16f3fe4a126cf15ed0d0ed45c3a69/annotatedyaml-1.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4b9056f207ee1a9b2cbb53143b06d38ee0dd56eafbf42d21f834d031cf98c827", size = 131073, upload-time = "2025-08-01T19:23:04.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/3a/dbd3690dc5cc6a28e55a72ab449a81870f650e53ed136bf8d71a7561bdf3/annotatedyaml-1.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3baca49035d6d825856af7f97b871742cc19594cd299d03e25a9ea9cc1e586", size = 131122, upload-time = "2025-08-01T19:23:05.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/59/5f3f111a0d8ea6a53ff807672da525c21320bdbddd2abc99e13955fba8c6/annotatedyaml-1.0.0-cp314-cp314t-win32.whl", hash = "sha256:bf1b53367b8efd3cd6cef16482318aa4c9caa79ace99a75e3762ccfef121c338", size = 104890, upload-time = "2025-08-01T19:23:06.895Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/32/0254acfeda2f745699757bb878ce4425e9e5dd8f356b03dfa3157bcfa69d/annotatedyaml-1.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:fc33c172a4b980c70ce29bf1fc150dbbe249f248c266b51f9e631ad2bf01f4b4", size = 114351, upload-time = "2025-08-01T19:23:07.962Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -81,9 +164,9 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -93,18 +176,30 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505, upload-time = "2023-08-04T07:54:58.425Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deprecated"
|
||||
version = "1.2.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -115,9 +210,34 @@ dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "untokenize" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/7b/ee08cb5fe2627ed0b6f0cc4a1c6be6c9c71de5a3e9785de8174273fc3128/docformatter-1.7.7.tar.gz", hash = "sha256:ea0e1e8867e5af468dfc3f9e947b92230a55be9ec17cd1609556387bffac7978", size = 26587 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/7b/ee08cb5fe2627ed0b6f0cc4a1c6be6c9c71de5a3e9785de8174273fc3128/docformatter-1.7.7.tar.gz", hash = "sha256:ea0e1e8867e5af468dfc3f9e947b92230a55be9ec17cd1609556387bffac7978", size = 26587, upload-time = "2025-05-11T04:54:04.356Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl", hash = "sha256:7af49f8a46346a77858f6651f431b882c503c2f4442c8b4524b920c863277834", size = 33525 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b4/a7ec1eaee86761a9dbfd339732b4706db3c6b65e970c12f0f56cfcce3dcf/docformatter-1.7.7-py3-none-any.whl", hash = "sha256:7af49f8a46346a77858f6651f431b882c503c2f4442c8b4524b920c863277834", size = 33525, upload-time = "2025-05-11T04:54:03.353Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.9.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.117.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/d9788300deaf416178f61fb3c2ceb16b7d0dc9f82a08fdb87a5e64ee3cc7/fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a", size = 307155, upload-time = "2025-09-20T20:16:56.663Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/45/d9d3e8eeefbe93be1c50060a9d9a9f366dba66f288bb518a9566a23a8631/fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552", size = 95959, upload-time = "2025-09-20T20:16:53.661Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -130,25 +250,58 @@ dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/84/79ca1e01337fe898cd303ac8d51151b4bea4891028b93ae5bf5e9cc911a9/generateDS-2.44.3-py3-none-any.whl", hash = "sha256:ae5db7105ca777182ba6549118c9aba1690ea341400af13ffbdbfbe1bc022299", size = 147394 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/84/79ca1e01337fe898cd303ac8d51151b4bea4891028b93ae5bf5e9cc911a9/generateDS-2.44.3-py3-none-any.whl", hash = "sha256:ae5db7105ca777182ba6549118c9aba1690ea341400af13ffbdbfbe1bc022299", size = 147394, upload-time = "2024-10-08T21:54:34.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
|
||||
{ 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/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/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/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/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
|
||||
{ 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/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/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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -158,99 +311,154 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "limits"
|
||||
version = "5.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "deprecated" },
|
||||
{ name = "packaging" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/17/7a2e9378c8b8bd4efe3573fd18d2793ad2a37051af5ccce94550a4e5d62d/limits-5.5.0.tar.gz", hash = "sha256:ee269fedb078a904608b264424d9ef4ab10555acc8d090b6fc1db70e913327ea", size = 95514, upload-time = "2025-08-05T18:23:54.771Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/68/ee314018c28da75ece5a639898b4745bd0687c0487fc465811f0c4b9cd44/limits-5.5.0-py3-none-any.whl", hash = "sha256:57217d01ffa5114f7e233d1f5e5bdc6fe60c9b24ade387bf4d5e83c5cf929bae", size = 60948, upload-time = "2025-08-05T18:23:53.335Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/bd/f9d01fd4132d81c6f43ab01983caea69ec9614b913c290a26738431a015d/lxml-6.0.1.tar.gz", hash = "sha256:2b3a882ebf27dd026df3801a87cf49ff791336e0f94b0fad195db77e01240690", size = 4070214 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8f/bd/f9d01fd4132d81c6f43ab01983caea69ec9614b913c290a26738431a015d/lxml-6.0.1.tar.gz", hash = "sha256:2b3a882ebf27dd026df3801a87cf49ff791336e0f94b0fad195db77e01240690", size = 4070214, upload-time = "2025-08-22T10:37:53.525Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/c4/cd757eeec4548e6652eff50b944079d18ce5f8182d2b2cf514e125e8fbcb/lxml-6.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:485eda5d81bb7358db96a83546949c5fe7474bec6c68ef3fa1fb61a584b00eea", size = 8405139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/99/0290bb86a7403893f5e9658490c705fcea103b9191f2039752b071b4ef07/lxml-6.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d12160adea318ce3d118f0b4fbdff7d1225c75fb7749429541b4d217b85c3f76", size = 4585954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a7/4bb54dd1e626342a0f7df6ec6ca44fdd5d0e100ace53acc00e9a689ead04/lxml-6.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48c8d335d8ab72f9265e7ba598ae5105a8272437403f4032107dbcb96d3f0b29", size = 4944052 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/8d/20f51cd07a7cbef6214675a8a5c62b2559a36d9303fe511645108887c458/lxml-6.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:405e7cf9dbdbb52722c231e0f1257214202dfa192327fab3de45fd62e0554082", size = 5098885 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/63/efceeee7245d45f97d548e48132258a36244d3c13c6e3ddbd04db95ff496/lxml-6.0.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:299a790d403335a6a057ade46f92612ebab87b223e4e8c5308059f2dc36f45ed", size = 5017542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5d/92cb3d3499f5caba17f7933e6be3b6c7de767b715081863337ced42eb5f2/lxml-6.0.1-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:48da704672f6f9c461e9a73250440c647638cc6ff9567ead4c3b1f189a604ee8", size = 5347303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/f8/606fa16a05d7ef5e916c6481c634f40870db605caffed9d08b1a4fb6b989/lxml-6.0.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21e364e1bb731489e3f4d51db416f991a5d5da5d88184728d80ecfb0904b1d68", size = 5641055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/01/15d5fc74ebb49eac4e5df031fbc50713dcc081f4e0068ed963a510b7d457/lxml-6.0.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bce45a2c32032afddbd84ed8ab092130649acb935536ef7a9559636ce7ffd4a", size = 5242719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/a5/1b85e2aaaf8deaa67e04c33bddb41f8e73d07a077bf9db677cec7128bfb4/lxml-6.0.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:fa164387ff20ab0e575fa909b11b92ff1481e6876835014e70280769920c4433", size = 4717310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/23/f3bb1292f55a725814317172eeb296615db3becac8f1a059b53c51fc1da8/lxml-6.0.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7587ac5e000e1594e62278422c5783b34a82b22f27688b1074d71376424b73e8", size = 5254024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/be/4d768f581ccd0386d424bac615d9002d805df7cc8482ae07d529f60a3c1e/lxml-6.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:57478424ac4c9170eabf540237125e8d30fad1940648924c058e7bc9fb9cf6dd", size = 5055335 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/07/ed61d1a3e77d1a9f856c4fab15ee5c09a2853fb7af13b866bb469a3a6d42/lxml-6.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:09c74afc7786c10dd6afaa0be2e4805866beadc18f1d843cf517a7851151b499", size = 4784864 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/37/77e7971212e5c38a55431744f79dff27fd751771775165caea096d055ca4/lxml-6.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7fd70681aeed83b196482d42a9b0dc5b13bab55668d09ad75ed26dff3be5a2f5", size = 5657173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a3/e98806d483941cd9061cc838b1169626acef7b2807261fbe5e382fcef881/lxml-6.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:10a72e456319b030b3dd900df6b1f19d89adf06ebb688821636dc406788cf6ac", size = 5245896 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/de/9bb5a05e42e8623bf06b4638931ea8c8f5eb5a020fe31703abdbd2e83547/lxml-6.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0fa45fb5f55111ce75b56c703843b36baaf65908f8b8d2fbbc0e249dbc127ed", size = 5267417 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/43/c1cb2a7c67226266c463ef8a53b82d42607228beb763b5fbf4867e88a21f/lxml-6.0.1-cp313-cp313-win32.whl", hash = "sha256:01dab65641201e00c69338c9c2b8a0f2f484b6b3a22d10779bb417599fae32b5", size = 3610051 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/96/6a6c3b8aa480639c1a0b9b6faf2a63fb73ab79ffcd2a91cf28745faa22de/lxml-6.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:bdf8f7c8502552d7bff9e4c98971910a0a59f60f88b5048f608d0a1a75e94d1c", size = 4009325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/66/622e8515121e1fd773e3738dae71b8df14b12006d9fb554ce90886689fd0/lxml-6.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6aeca75959426b9fd8d4782c28723ba224fe07cfa9f26a141004210528dcbe2", size = 3670443 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/e3/b7eb612ce07abe766918a7e581ec6a0e5212352194001fd287c3ace945f0/lxml-6.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:29b0e849ec7030e3ecb6112564c9f7ad6881e3b2375dd4a0c486c5c1f3a33859", size = 8426160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/8f/ab3639a33595cf284fe733c6526da2ca3afbc5fd7f244ae67f3303cec654/lxml-6.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:02a0f7e629f73cc0be598c8b0611bf28ec3b948c549578a26111b01307fd4051", size = 4589288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/65/819d54f2e94d5c4458c1db8c1ccac9d05230b27c1038937d3d788eb406f9/lxml-6.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:beab5e54de016e730875f612ba51e54c331e2fa6dc78ecf9a5415fc90d619348", size = 4964523 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/4a/d4a74ce942e60025cdaa883c5a4478921a99ce8607fc3130f1e349a83b28/lxml-6.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a08aefecd19ecc4ebf053c27789dd92c87821df2583a4337131cf181a1dffa", size = 5101108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/48/67f15461884074edd58af17b1827b983644d1fae83b3d909e9045a08b61e/lxml-6.0.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36c8fa7e177649470bc3dcf7eae6bee1e4984aaee496b9ccbf30e97ac4127fa2", size = 5053498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d4/ec1bf1614828a5492f4af0b6a9ee2eb3e92440aea3ac4fa158e5228b772b/lxml-6.0.1-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:5d08e0f1af6916267bb7eff21c09fa105620f07712424aaae09e8cb5dd4164d1", size = 5351057 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/2b/c85929dacac08821f2100cea3eb258ce5c8804a4e32b774f50ebd7592850/lxml-6.0.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9705cdfc05142f8c38c97a61bd3a29581ceceb973a014e302ee4a73cc6632476", size = 5671579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/36/cf544d75c269b9aad16752fd9f02d8e171c5a493ca225cb46bb7ba72868c/lxml-6.0.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74555e2da7c1636e30bff4e6e38d862a634cf020ffa591f1f63da96bf8b34772", size = 5250403 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e8/83dbc946ee598fd75fdeae6151a725ddeaab39bb321354a9468d4c9f44f3/lxml-6.0.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e38b5f94c5a2a5dadaddd50084098dfd005e5a2a56cd200aaf5e0a20e8941782", size = 4696712 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/72/889c633b47c06205743ba935f4d1f5aa4eb7f0325d701ed2b0540df1b004/lxml-6.0.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5ec101a92ddacb4791977acfc86c1afd624c032974bfb6a21269d1083c9bc49", size = 5268177 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/b6/f42a21a1428479b66ea0da7bd13e370436aecaff0cfe93270c7e165bd2a4/lxml-6.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5c17e70c82fd777df586c12114bbe56e4e6f823a971814fd40dec9c0de518772", size = 5094648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b0/5f8c1e8890e2ee1c2053c2eadd1cb0e4b79e2304e2912385f6ca666f48b1/lxml-6.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:45fdd0415a0c3d91640b5d7a650a8f37410966a2e9afebb35979d06166fd010e", size = 4745220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f9/820b5125660dae489ca3a21a36d9da2e75dd6b5ffe922088f94bbff3b8a0/lxml-6.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d417eba28981e720a14fcb98f95e44e7a772fe25982e584db38e5d3b6ee02e79", size = 5692913 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/8e/a557fae9eec236618aecf9ff35fec18df41b6556d825f3ad6017d9f6e878/lxml-6.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8e5d116b9e59be7934febb12c41cce2038491ec8fdb743aeacaaf36d6e7597e4", size = 5259816 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/fd/b266cfaab81d93a539040be699b5854dd24c84e523a1711ee5f615aa7000/lxml-6.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c238f0d0d40fdcb695c439fe5787fa69d40f45789326b3bb6ef0d61c4b588d6e", size = 5276162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/6c/6f9610fbf1de002048e80585ea4719591921a0316a8565968737d9f125ca/lxml-6.0.1-cp314-cp314-win32.whl", hash = "sha256:537b6cf1c5ab88cfd159195d412edb3e434fee880f206cbe68dff9c40e17a68a", size = 3669595 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/a5/506775e3988677db24dc75a7b03e04038e0b3d114ccd4bccea4ce0116c15/lxml-6.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:911d0a2bb3ef3df55b3d97ab325a9ca7e438d5112c102b8495321105d25a441b", size = 4079818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/44/9613f300201b8700215856e5edd056d4e58dd23368699196b58877d4408b/lxml-6.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:2834377b0145a471a654d699bdb3a2155312de492142ef5a1d426af2c60a0a31", size = 3753901 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/c4/cd757eeec4548e6652eff50b944079d18ce5f8182d2b2cf514e125e8fbcb/lxml-6.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:485eda5d81bb7358db96a83546949c5fe7474bec6c68ef3fa1fb61a584b00eea", size = 8405139, upload-time = "2025-08-22T10:33:34.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/99/0290bb86a7403893f5e9658490c705fcea103b9191f2039752b071b4ef07/lxml-6.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d12160adea318ce3d118f0b4fbdff7d1225c75fb7749429541b4d217b85c3f76", size = 4585954, upload-time = "2025-08-22T10:33:36.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/a7/4bb54dd1e626342a0f7df6ec6ca44fdd5d0e100ace53acc00e9a689ead04/lxml-6.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48c8d335d8ab72f9265e7ba598ae5105a8272437403f4032107dbcb96d3f0b29", size = 4944052, upload-time = "2025-08-22T10:33:38.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/8d/20f51cd07a7cbef6214675a8a5c62b2559a36d9303fe511645108887c458/lxml-6.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:405e7cf9dbdbb52722c231e0f1257214202dfa192327fab3de45fd62e0554082", size = 5098885, upload-time = "2025-08-22T10:33:40.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/63/efceeee7245d45f97d548e48132258a36244d3c13c6e3ddbd04db95ff496/lxml-6.0.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:299a790d403335a6a057ade46f92612ebab87b223e4e8c5308059f2dc36f45ed", size = 5017542, upload-time = "2025-08-22T10:33:41.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5d/92cb3d3499f5caba17f7933e6be3b6c7de767b715081863337ced42eb5f2/lxml-6.0.1-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:48da704672f6f9c461e9a73250440c647638cc6ff9567ead4c3b1f189a604ee8", size = 5347303, upload-time = "2025-08-22T10:33:43.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/f8/606fa16a05d7ef5e916c6481c634f40870db605caffed9d08b1a4fb6b989/lxml-6.0.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21e364e1bb731489e3f4d51db416f991a5d5da5d88184728d80ecfb0904b1d68", size = 5641055, upload-time = "2025-08-22T10:33:45.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/01/15d5fc74ebb49eac4e5df031fbc50713dcc081f4e0068ed963a510b7d457/lxml-6.0.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bce45a2c32032afddbd84ed8ab092130649acb935536ef7a9559636ce7ffd4a", size = 5242719, upload-time = "2025-08-22T10:33:48.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/a5/1b85e2aaaf8deaa67e04c33bddb41f8e73d07a077bf9db677cec7128bfb4/lxml-6.0.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:fa164387ff20ab0e575fa909b11b92ff1481e6876835014e70280769920c4433", size = 4717310, upload-time = "2025-08-22T10:33:49.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/23/f3bb1292f55a725814317172eeb296615db3becac8f1a059b53c51fc1da8/lxml-6.0.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7587ac5e000e1594e62278422c5783b34a82b22f27688b1074d71376424b73e8", size = 5254024, upload-time = "2025-08-22T10:33:52.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/be/4d768f581ccd0386d424bac615d9002d805df7cc8482ae07d529f60a3c1e/lxml-6.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:57478424ac4c9170eabf540237125e8d30fad1940648924c058e7bc9fb9cf6dd", size = 5055335, upload-time = "2025-08-22T10:33:54.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/07/ed61d1a3e77d1a9f856c4fab15ee5c09a2853fb7af13b866bb469a3a6d42/lxml-6.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:09c74afc7786c10dd6afaa0be2e4805866beadc18f1d843cf517a7851151b499", size = 4784864, upload-time = "2025-08-22T10:33:56.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/37/77e7971212e5c38a55431744f79dff27fd751771775165caea096d055ca4/lxml-6.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7fd70681aeed83b196482d42a9b0dc5b13bab55668d09ad75ed26dff3be5a2f5", size = 5657173, upload-time = "2025-08-22T10:33:58.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a3/e98806d483941cd9061cc838b1169626acef7b2807261fbe5e382fcef881/lxml-6.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:10a72e456319b030b3dd900df6b1f19d89adf06ebb688821636dc406788cf6ac", size = 5245896, upload-time = "2025-08-22T10:34:00.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/de/9bb5a05e42e8623bf06b4638931ea8c8f5eb5a020fe31703abdbd2e83547/lxml-6.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0fa45fb5f55111ce75b56c703843b36baaf65908f8b8d2fbbc0e249dbc127ed", size = 5267417, upload-time = "2025-08-22T10:34:02.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/43/c1cb2a7c67226266c463ef8a53b82d42607228beb763b5fbf4867e88a21f/lxml-6.0.1-cp313-cp313-win32.whl", hash = "sha256:01dab65641201e00c69338c9c2b8a0f2f484b6b3a22d10779bb417599fae32b5", size = 3610051, upload-time = "2025-08-22T10:34:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/96/6a6c3b8aa480639c1a0b9b6faf2a63fb73ab79ffcd2a91cf28745faa22de/lxml-6.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:bdf8f7c8502552d7bff9e4c98971910a0a59f60f88b5048f608d0a1a75e94d1c", size = 4009325, upload-time = "2025-08-22T10:34:06.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/66/622e8515121e1fd773e3738dae71b8df14b12006d9fb554ce90886689fd0/lxml-6.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:a6aeca75959426b9fd8d4782c28723ba224fe07cfa9f26a141004210528dcbe2", size = 3670443, upload-time = "2025-08-22T10:34:07.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/e3/b7eb612ce07abe766918a7e581ec6a0e5212352194001fd287c3ace945f0/lxml-6.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:29b0e849ec7030e3ecb6112564c9f7ad6881e3b2375dd4a0c486c5c1f3a33859", size = 8426160, upload-time = "2025-08-22T10:34:10.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/8f/ab3639a33595cf284fe733c6526da2ca3afbc5fd7f244ae67f3303cec654/lxml-6.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:02a0f7e629f73cc0be598c8b0611bf28ec3b948c549578a26111b01307fd4051", size = 4589288, upload-time = "2025-08-22T10:34:12.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/65/819d54f2e94d5c4458c1db8c1ccac9d05230b27c1038937d3d788eb406f9/lxml-6.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:beab5e54de016e730875f612ba51e54c331e2fa6dc78ecf9a5415fc90d619348", size = 4964523, upload-time = "2025-08-22T10:34:15.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/4a/d4a74ce942e60025cdaa883c5a4478921a99ce8607fc3130f1e349a83b28/lxml-6.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a08aefecd19ecc4ebf053c27789dd92c87821df2583a4337131cf181a1dffa", size = 5101108, upload-time = "2025-08-22T10:34:17.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/48/67f15461884074edd58af17b1827b983644d1fae83b3d909e9045a08b61e/lxml-6.0.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36c8fa7e177649470bc3dcf7eae6bee1e4984aaee496b9ccbf30e97ac4127fa2", size = 5053498, upload-time = "2025-08-22T10:34:19.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d4/ec1bf1614828a5492f4af0b6a9ee2eb3e92440aea3ac4fa158e5228b772b/lxml-6.0.1-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:5d08e0f1af6916267bb7eff21c09fa105620f07712424aaae09e8cb5dd4164d1", size = 5351057, upload-time = "2025-08-22T10:34:21.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/2b/c85929dacac08821f2100cea3eb258ce5c8804a4e32b774f50ebd7592850/lxml-6.0.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9705cdfc05142f8c38c97a61bd3a29581ceceb973a014e302ee4a73cc6632476", size = 5671579, upload-time = "2025-08-22T10:34:23.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/36/cf544d75c269b9aad16752fd9f02d8e171c5a493ca225cb46bb7ba72868c/lxml-6.0.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74555e2da7c1636e30bff4e6e38d862a634cf020ffa591f1f63da96bf8b34772", size = 5250403, upload-time = "2025-08-22T10:34:25.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e8/83dbc946ee598fd75fdeae6151a725ddeaab39bb321354a9468d4c9f44f3/lxml-6.0.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:e38b5f94c5a2a5dadaddd50084098dfd005e5a2a56cd200aaf5e0a20e8941782", size = 4696712, upload-time = "2025-08-22T10:34:27.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/72/889c633b47c06205743ba935f4d1f5aa4eb7f0325d701ed2b0540df1b004/lxml-6.0.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5ec101a92ddacb4791977acfc86c1afd624c032974bfb6a21269d1083c9bc49", size = 5268177, upload-time = "2025-08-22T10:34:29.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/b6/f42a21a1428479b66ea0da7bd13e370436aecaff0cfe93270c7e165bd2a4/lxml-6.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5c17e70c82fd777df586c12114bbe56e4e6f823a971814fd40dec9c0de518772", size = 5094648, upload-time = "2025-08-22T10:34:31.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b0/5f8c1e8890e2ee1c2053c2eadd1cb0e4b79e2304e2912385f6ca666f48b1/lxml-6.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:45fdd0415a0c3d91640b5d7a650a8f37410966a2e9afebb35979d06166fd010e", size = 4745220, upload-time = "2025-08-22T10:34:33.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/f9/820b5125660dae489ca3a21a36d9da2e75dd6b5ffe922088f94bbff3b8a0/lxml-6.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d417eba28981e720a14fcb98f95e44e7a772fe25982e584db38e5d3b6ee02e79", size = 5692913, upload-time = "2025-08-22T10:34:35.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/8e/a557fae9eec236618aecf9ff35fec18df41b6556d825f3ad6017d9f6e878/lxml-6.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8e5d116b9e59be7934febb12c41cce2038491ec8fdb743aeacaaf36d6e7597e4", size = 5259816, upload-time = "2025-08-22T10:34:37.482Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/fd/b266cfaab81d93a539040be699b5854dd24c84e523a1711ee5f615aa7000/lxml-6.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c238f0d0d40fdcb695c439fe5787fa69d40f45789326b3bb6ef0d61c4b588d6e", size = 5276162, upload-time = "2025-08-22T10:34:39.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/6c/6f9610fbf1de002048e80585ea4719591921a0316a8565968737d9f125ca/lxml-6.0.1-cp314-cp314-win32.whl", hash = "sha256:537b6cf1c5ab88cfd159195d412edb3e434fee880f206cbe68dff9c40e17a68a", size = 3669595, upload-time = "2025-08-22T10:34:41.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/a5/506775e3988677db24dc75a7b03e04038e0b3d114ccd4bccea4ce0116c15/lxml-6.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:911d0a2bb3ef3df55b3d97ab325a9ca7e438d5112c102b8495321105d25a441b", size = 4079818, upload-time = "2025-08-22T10:34:44.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/44/9613f300201b8700215856e5edd056d4e58dd23368699196b58877d4408b/lxml-6.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:2834377b0145a471a654d699bdb3a2155312de492142ef5a1d426af2c60a0a31", size = 3753901, upload-time = "2025-08-22T10:34:45.799Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||
{ 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 = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||
{ 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 = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -263,9 +471,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -275,34 +483,34 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -316,9 +524,63 @@ dependencies = [
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" }
|
||||
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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "6.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -331,62 +593,116 @@ dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ab/33/c8e89216845615d14d2d42ba2bee404e7206a8db782f33400754f3799f05/ruff-0.13.1.tar.gz", hash = "sha256:88074c3849087f153d4bb22e92243ad4c1b366d7055f98726bc19aa08dc12d51", size = 5397987, upload-time = "2025-09-18T19:52:44.33Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/41/ca37e340938f45cfb8557a97a5c347e718ef34702546b174e5300dbb1f28/ruff-0.13.1-py3-none-linux_armv6l.whl", hash = "sha256:b2abff595cc3cbfa55e509d89439b5a09a6ee3c252d92020bd2de240836cf45b", size = 12304308, upload-time = "2025-09-18T19:51:56.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/84/ba378ef4129415066c3e1c80d84e539a0d52feb250685091f874804f28af/ruff-0.13.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4ee9f4249bf7f8bb3984c41bfaf6a658162cdb1b22e3103eabc7dd1dc5579334", size = 12937258, upload-time = "2025-09-18T19:52:00.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/b6/ec5e4559ae0ad955515c176910d6d7c93edcbc0ed1a3195a41179c58431d/ruff-0.13.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c5da4af5f6418c07d75e6f3224e08147441f5d1eac2e6ce10dcce5e616a3bae", size = 12214554, upload-time = "2025-09-18T19:52:02.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/d6/cb3e3b4f03b9b0c4d4d8f06126d34b3394f6b4d764912fe80a1300696ef6/ruff-0.13.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80524f84a01355a59a93cef98d804e2137639823bcee2931f5028e71134a954e", size = 12448181, upload-time = "2025-09-18T19:52:05.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ea/bf60cb46d7ade706a246cd3fb99e4cfe854efa3dfbe530d049c684da24ff/ruff-0.13.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff7f5ce8d7988767dd46a148192a14d0f48d1baea733f055d9064875c7d50389", size = 12104599, upload-time = "2025-09-18T19:52:07.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/3e/05f72f4c3d3a69e65d55a13e1dd1ade76c106d8546e7e54501d31f1dc54a/ruff-0.13.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c55d84715061f8b05469cdc9a446aa6c7294cd4bd55e86a89e572dba14374f8c", size = 13791178, upload-time = "2025-09-18T19:52:10.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/e7/01b1fc403dd45d6cfe600725270ecc6a8f8a48a55bc6521ad820ed3ceaf8/ruff-0.13.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ac57fed932d90fa1624c946dc67a0a3388d65a7edc7d2d8e4ca7bddaa789b3b0", size = 14814474, upload-time = "2025-09-18T19:52:12.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/92/d9e183d4ed6185a8df2ce9faa3f22e80e95b5f88d9cc3d86a6d94331da3f/ruff-0.13.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c366a71d5b4f41f86a008694f7a0d75fe409ec298685ff72dc882f882d532e36", size = 14217531, upload-time = "2025-09-18T19:52:15.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/4a/6ddb1b11d60888be224d721e01bdd2d81faaf1720592858ab8bac3600466/ruff-0.13.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4ea9d1b5ad3e7a83ee8ebb1229c33e5fe771e833d6d3dcfca7b77d95b060d38", size = 13265267, upload-time = "2025-09-18T19:52:17.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/98/3f1d18a8d9ea33ef2ad508f0417fcb182c99b23258ec5e53d15db8289809/ruff-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0f70202996055b555d3d74b626406476cc692f37b13bac8828acff058c9966a", size = 13243120, upload-time = "2025-09-18T19:52:20.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/86/b6ce62ce9c12765fa6c65078d1938d2490b2b1d9273d0de384952b43c490/ruff-0.13.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f8cff7a105dad631085d9505b491db33848007d6b487c3c1979dd8d9b2963783", size = 13443084, upload-time = "2025-09-18T19:52:23.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/6e/af7943466a41338d04503fb5a81b2fd07251bd272f546622e5b1599a7976/ruff-0.13.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9761e84255443316a258dd7dfbd9bfb59c756e52237ed42494917b2577697c6a", size = 12295105, upload-time = "2025-09-18T19:52:25.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/97/0249b9a24f0f3ebd12f007e81c87cec6d311de566885e9309fcbac5b24cc/ruff-0.13.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3d376a88c3102ef228b102211ef4a6d13df330cb0f5ca56fdac04ccec2a99700", size = 12072284, upload-time = "2025-09-18T19:52:27.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/85/0b64693b2c99d62ae65236ef74508ba39c3febd01466ef7f354885e5050c/ruff-0.13.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cbefd60082b517a82c6ec8836989775ac05f8991715d228b3c1d86ccc7df7dae", size = 12970314, upload-time = "2025-09-18T19:52:30.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/fc/342e9f28179915d28b3747b7654f932ca472afbf7090fc0c4011e802f494/ruff-0.13.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd16b9a5a499fe73f3c2ef09a7885cb1d97058614d601809d37c422ed1525317", size = 13422360, upload-time = "2025-09-18T19:52:32.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/54/6177a0dc10bce6f43e392a2192e6018755473283d0cf43cc7e6afc182aea/ruff-0.13.1-py3-none-win32.whl", hash = "sha256:55e9efa692d7cb18580279f1fbb525146adc401f40735edf0aaeabd93099f9a0", size = 12178448, upload-time = "2025-09-18T19:52:35.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/51/c6a3a33d9938007b8bdc8ca852ecc8d810a407fb513ab08e34af12dc7c24/ruff-0.13.1-py3-none-win_amd64.whl", hash = "sha256:3a3fb595287ee556de947183489f636b9f76a72f0fa9c028bdcabf5bab2cc5e5", size = 13286458, upload-time = "2025-09-18T19:52:38.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/04/afc078a12cf68592345b1e2d6ecdff837d286bac023d7a22c54c7a698c5b/ruff-0.13.1-py3-none-win_arm64.whl", hash = "sha256:c0bae9ffd92d54e03c2bf266f466da0a65e145f298ee5b5846ed435f6a00518a", size = 12437893, upload-time = "2025-09-18T19:52:41.283Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slowapi"
|
||||
version = "0.1.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "limits" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.43"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.48.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toposort"
|
||||
version = "1.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/19/8e955d90985ecbd3b9adb2a759753a6840da2dff3c569d412b2c9217678b/toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd", size = 11132 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/19/8e955d90985ecbd3b9adb2a759753a6840da2dff3c569d412b2c9217678b/toposort-1.10.tar.gz", hash = "sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd", size = 11132, upload-time = "2023-02-27T13:59:51.834Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/17/57b444fd314d5e1593350b9a31d000e7411ba8e17ce12dc7ad54ca76b810/toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87", size = 8500 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/17/57b444fd314d5e1593350b9a31d000e7411ba8e17ce12dc7ad54ca76b810/toposort-1.10-py3-none-any.whl", hash = "sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87", size = 8500, upload-time = "2023-02-25T20:07:06.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -396,24 +712,85 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
|
||||
{ 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 = "untokenize"
|
||||
version = "0.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz", hash = "sha256:3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2", size = 3099 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/46/e7cea8159199096e1df52da20a57a6665da80c37fb8aeb848a3e47442c32/untokenize-0.1.1.tar.gz", hash = "sha256:3865dbbbb8efb4bb5eaa72f1be7f3e0be00ea8b7f125c69cbd1f5fda926f37a2", size = 3099, upload-time = "2014-02-08T16:30:40.631Z" }
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.37.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "voluptuous"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/af/a54ce0fb6f1d867e0b9f0efe5f082a691f51ccf705188fca67a3ecefd7f4/voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa", size = 51651, upload-time = "2024-07-02T19:10:00.528Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a8/8f9cc6749331186e6a513bfe3745454f81d25f6e34c6024f88f80c71ed28/voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566", size = 31349, upload-time = "2024-07-02T19:09:58.125Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "1.17.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -423,9 +800,9 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/cf/d393286e40f7574c5d662a3ceefcf8e4cd65e73af6e54db0585c5b17c541/xsdata-25.7.tar.gz", hash = "sha256:1291ef759f4663baadb86562be4c25ebfc0003ca0debae3042b0067663f0c548", size = 345469 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/cf/d393286e40f7574c5d662a3ceefcf8e4cd65e73af6e54db0585c5b17c541/xsdata-25.7.tar.gz", hash = "sha256:1291ef759f4663baadb86562be4c25ebfc0003ca0debae3042b0067663f0c548", size = 345469, upload-time = "2025-07-06T16:40:03.19Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/10/c866e7b0fd57c92a4d5676884b81383005d81f8d7f07f1ac17e9c0ab3643/xsdata-25.7-py3-none-any.whl", hash = "sha256:d50b8c39389fd2b7283767a68a80cbf3bc51a3ede9cc3fefb30e84a52c999a9d", size = 234469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/10/c866e7b0fd57c92a4d5676884b81383005d81f8d7f07f1ac17e9c0ab3643/xsdata-25.7-py3-none-any.whl", hash = "sha256:d50b8c39389fd2b7283767a68a80cbf3bc51a3ede9cc3fefb30e84a52c999a9d", size = 234469, upload-time = "2025-07-06T16:40:01.656Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -452,9 +829,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "xsdata" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/5e/6bc728d70460d9ad3982d05c3765179e3584fee6fa523d57b242e6e4c50f/xsdata_pydantic-24.5.tar.gz", hash = "sha256:e3c8758133195657ece578537eda6c7ebd8419f77abf6b90fd4ced96e348129b", size = 18763 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/5e/6bc728d70460d9ad3982d05c3765179e3584fee6fa523d57b242e6e4c50f/xsdata_pydantic-24.5.tar.gz", hash = "sha256:e3c8758133195657ece578537eda6c7ebd8419f77abf6b90fd4ced96e348129b", size = 18763, upload-time = "2024-05-08T17:49:48.28Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/7b/785fe71aa1138d7380ab3926cbb9571896d56544901c320953ff8a586926/xsdata_pydantic-24.5-py3-none-any.whl", hash = "sha256:bb6da7d3445d655640096c65c1b11037153b19df533da89553f24247ef352cd0", size = 8891 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/7b/785fe71aa1138d7380ab3926cbb9571896d56544901c320953ff8a586926/xsdata_pydantic-24.5-py3-none-any.whl", hash = "sha256:bb6da7d3445d655640096c65c1b11037153b19df533da89553f24247ef352cd0", size = 8891, upload-time = "2024-05-08T17:49:46.408Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user