From 56a7d320c5114740bf749a7ae47a5c04ceda33ee Mon Sep 17 00:00:00 2001 From: milovann Date: Wed, 25 Mar 2026 10:36:30 +0100 Subject: [PATCH] release commit --- .gitignore | 6 + .python-version | 1 + AGENT.md | 40 ++ LICENSE | 21 + Makefile | 57 ++ README.md | 146 +++++ build_html.py | 309 ++++++++++ docs/README.md | 2 + docs/architecture.md | 124 ++++ docs/commands.md | 248 ++++++++ docs/history.md | 137 +++++ docs/ifc-extract.md | 133 +++++ docs/ifcfile.md | 160 ++++++ docs/install.md | 64 +++ docs/merge.md | 132 +++++ docs/merge_native.md | 152 +++++ docs/research.md | 226 ++++++++ docs/space_modifications.md | 119 ++++ docs/wasm.md | 162 ++++++ files/nginx/ifc-commit.conf | 19 + files/systemd/ifc-commit.service | 12 + files/yaml/copy_columns.yaml | 20 + files/yaml/duplex.yaml | 39 ++ files/yaml/duplex_history.yaml | 9 + files/yaml/office-storey.yaml | 10 + files/yaml/office-walls.yaml | 16 + files/yaml/office.yaml | 12 + ifccommit.py | 907 ++++++++++++++++++++++++++++++ pyproject.toml | 28 + scripts/clone_demo_repo.sh | 13 + scripts/release.sh | 49 ++ scripts/reset_demo_repo.sh | 70 +++ uv.lock | 932 +++++++++++++++++++++++++++++++ webapp/__init__.py | 0 webapp/forge.py | 115 ++++ webapp/main.py | 299 ++++++++++ webapp/pipeline.py | 267 +++++++++ webapp/schema.py | 196 +++++++ 38 files changed, 5252 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 AGENT.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 build_html.py create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/commands.md create mode 100644 docs/history.md create mode 100644 docs/ifc-extract.md create mode 100644 docs/ifcfile.md create mode 100644 docs/install.md create mode 100644 docs/merge.md create mode 100644 docs/merge_native.md create mode 100644 docs/research.md create mode 100644 docs/space_modifications.md create mode 100644 docs/wasm.md create mode 100644 files/nginx/ifc-commit.conf create mode 100644 files/systemd/ifc-commit.service create mode 100644 files/yaml/copy_columns.yaml create mode 100644 files/yaml/duplex.yaml create mode 100644 files/yaml/duplex_history.yaml create mode 100644 files/yaml/office-storey.yaml create mode 100644 files/yaml/office-walls.yaml create mode 100644 files/yaml/office.yaml create mode 100644 ifccommit.py create mode 100644 pyproject.toml create mode 100644 scripts/clone_demo_repo.sh create mode 100755 scripts/release.sh create mode 100755 scripts/reset_demo_repo.sh create mode 100644 uv.lock create mode 100644 webapp/__init__.py create mode 100644 webapp/forge.py create mode 100644 webapp/main.py create mode 100644 webapp/pipeline.py create mode 100644 webapp/schema.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1789bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +tests/__pycache__ +dist/ +demo/ +index.html +research.html diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..38f1322 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,40 @@ +# AGENT.md + +This file provides additional guidance for AI agents working on this repository. + +## Project Overview + +**ifc-commit** is a CLI tool for slicing, inspecting, and composing IFC (Industry Foundation Classes) files used in Building Information Modeling (BIM). + +## Key Files + +| File | Purpose | +|------|---------| +| `ifccommit.py` | Main CLI entry point (~1100 lines) | +| `webapp/main.py` | FastAPI web server | +| `webapp/pipeline.py` | Modification pipeline logic | +| `pyproject.toml` | Project dependencies and scripts | + +## Commands + +```bash +uv run ifccommit.py list # list IFC types and counts +uv run ifccommit.py info # dump element attributes +uv run ifccommit.py extract [...] # extract to new file +uv run ifccommit.py space # extract space contents +uv run ifccommit.py split [type] # split by storey +uv run ifccommit.py move --x [--y] [--z] # translate element +uv run ifccommit.py insert # merge files +uv run ifccommit.py replace # replace space +``` + +## Testing + +```bash +make test # run pytest +make test-pipeline # run full pipeline then pytest +``` + +## Important Notes + +- Use `uv` for package management diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..73d2a74 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Milovann Yanatchkov (rvba) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1e7a8cb --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +.PHONY: clean html build restart \ + web web-dev kill \ + install-service start stop status \ + reset-git release clone-demo + +# ── Website ─────────────────────────────────────────────────────────────────── + +html: + uv run build_html.py + +build: html + +# ── Web app ────────────────────────────────────────────────────────────────── + +web: + uv run ifccommit-web + +web-dev: + uv run uvicorn webapp.main:app --reload --host 127.0.0.1 --port 8095 + +kill: + -pkill -f "uvicorn webapp.main:app" 2>/dev/null; true + +start: build + sudo systemctl start ifc-commit + +restart: build + sudo systemctl restart ifc-commit + +stop: + sudo systemctl stop ifc-commit + +status: + journalctl -u ifc-commit -f + +install-service: + sed "s|{WORKDIR}|$(shell pwd)|g; s|{USER}|$(shell whoami)|g" files/systemd/ifc-commit.service | sudo tee /etc/systemd/system/ifc-commit.service + sudo ln -sf $(shell pwd)/files/nginx/ifc-commit.conf /etc/nginx/sites-enabled/ifc-commit.conf + sudo systemctl daemon-reload + sudo systemctl enable --now ifc-commit + sudo nginx -t && sudo systemctl reload nginx + +# ── Release ─────────────────────────────────────────────────────────────────── + +release: + bash scripts/release.sh + +# ── Housekeeping ────────────────────────────────────────────────────────────── + +reset-git: + bash scripts/reset_demo_repo.sh + +clean: + rm -rf dist/ + +clone-demo: + bash scripts/clone_demo_repo.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..af16e18 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# ifc-commit + +> Git-native workflows for IFC + +An experiment started at the [Porto OpenBIM Hackathon](https://www.buildingsmart.org/openbim-hackathon-porto-2026/). Live at [ifcc.gitaec.org](https://ifcc.gitaec.org). + +--- + +## Background + +IFC and the openBIM ecosystem have made significant progress in enabling data +exchange between the many disciplines involved in a construction project. +As workflows grow more complex — more actors, more iterations, more +concurrent modifications — a gap becomes apparent: there is no standard way +to track *who* changed *what*, *when*, and *why* across the lifetime of a model. + +Git has answered this question for software development for decades, with a +mature model for branching, ownership, and modification history. ifc-commit +explores how that same paradigm can be applied to IFC, with the goal of +contributing a useful building block to the openBIM ecosystem. + +--- + +## Overview + +ifc-commit is a tool for working with IFC files in a git-based workflow. +It allows parts of a model to be extracted, modified, and merged back — +operations that can be declared in a configuration file, executed +automatically, and committed to a repository with a full trace of what +changed and who triggered the change. + +The project covers three layers: + +- **Command-line interface** — composable operations for extracting, merging, + splitting, moving, and removing IFC elements. +- **Pipeline system** — simple configuration files that chain operations + together and commit the results back to a git repository on every run. +- **Provenance inside the model** — git metadata embedded directly in the IFC + file using standard property sets, so every touched element carries a full + record of who changed it and why. + +Built on [IfcOpenShell](https://ifcopenshell.org) and hosted on [gitaec.org](https://gitaec.org). + +--- + +## Git Inside IFC + +The central idea is to store git metadata directly inside the IFC file, using +mechanisms that already exist in the IFC specification. This way, any +IFC-aware tool — viewer, validator, or plugin — can read the provenance data +without any custom extension. + +Two layers are used: + +**File level.** `IfcApplication.Version` stores the current commit hash as a +lightweight stamp on the whole file. Every tool that reads `IfcOwnerHistory` +sees it automatically. + +> **Example** — what this looks like in the IFC STEP file: +> ``` +> #2=IFCAPPLICATION(#1,'a1b2c3d4','ifc-commit','ifc-commit'); +> ``` + +**Element level.** A `Pset_GitCommit` property set is attached to every +element touched by an operation. It carries the full commit context: hash, +message, author, date, and branch. + +> **Example** — reading the property set back in Python: +> ```python +> for rel in element.IsDefinedBy: +> if rel.is_a("IfcRelDefinesByProperties"): +> pset = rel.RelatingPropertyDefinition +> if pset.Name == "Pset_GitCommit": +> props = {p.Name: p.NominalValue.wrappedValue +> for p in pset.HasProperties} +> # props = { +> # "CommitHash": "a1b2c3d4f5e6c7b8", +> # "CommitMessage": "Fix wall thickness in level 2", +> # "CommitAuthor": "alice <***@***>", +> # "CommitDate": "2026-03-24T14:30:00Z", +> # "CommitBranch": "feature/level-2-walls", +> # } +> ``` + +Unknown property sets are silently preserved by Revit, ArchiCAD, and other +tools, so the metadata travels with the model through the full exchange chain. + +This makes it possible to apply the full git workflow to BIM: **branches, +forks, hotfixes, pull requests, and merge reviews** — with every element +carrying a traceable record of who changed it and from which branch. + +--- + +## Pipeline System + +An `ifccommit.yaml` file in any repository declares which operations to run: + +```yaml +operations: + - name: extract-structure + command: extract + input: models/full.ifc + output: dist/structure.ifc + types: [IfcWall, IfcSlab] + + - name: split-by-storey + command: split + input: models/full.ifc + outdir: dist/storeys/ + + - name: apply-modification + command: replace + base: models/full.ifc + space: A102 + part: dist/team_updates.ifc + output: dist/merged.ifc +``` + +The web app fetches this file from the forge, shows a preview of the +operations, and streams execution logs in real time while committing the +results back to the repository. + +--- + +## IFC Compliance + +A deliberate design constraint is that ifc-commit introduces no proprietary +extensions. Every mechanism it relies on is already part of the IFC +specification: + +- **`IfcOwnerHistory`** — the native per-element change record, carrying + timestamps, authorship, and change action. +- **`Pset_GitCommit`** — a custom property set following the standard + extension pattern. Tools that do not recognise it silently preserve it; + tools that do can surface full git provenance per element. +- **`IfcApplication.Version`** — a zero-overhead file-level stamp holding + the current commit hash. + +Because custom property sets survive round-trips through Revit, ArchiCAD, and +other tools, the metadata travels with the model through the full exchange +chain — without requiring any plugin or custom viewer. + +For a detailed account of the research behind this approach — the IFC +mechanisms surveyed, the tradeoffs considered, and the implementation — see +the [research notes](/research). + diff --git a/build_html.py b/build_html.py new file mode 100644 index 0000000..8c165ca --- /dev/null +++ b/build_html.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +"""Build HTML pages from Markdown sources.""" + +import re +from pathlib import Path +from markdown_it import MarkdownIt + +ROOT = Path(__file__).parent +md = MarkdownIt("commonmark").enable("table") + +CSS = """ + *, *::before, *::after { box-sizing: border-box; } + + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 1.7; + color: #1a1a1a; + background: #fff; + margin: 0; + padding: 0; + } + + .container { + max-width: 860px; + margin: 0 auto; + padding: 3rem 2rem 6rem; + } + + h1 { font-size: 2.4rem; margin-bottom: 1.2rem; line-height: 1; } + + a.title-btn { + display: inline-block; + background: #6366f1; + color: #fff; + font-size: 2.4rem; + font-weight: 700; + padding: 0.2em 0.5em; + border-radius: 8px; + text-decoration: none; + line-height: 1.2; + transition: background 0.15s; + } + a.title-btn:hover { background: #4f46e5; text-decoration: none; } + + h2 { font-size: 1.4rem; margin-top: 3rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.3rem; } + h3 { font-size: 1.1rem; margin-top: 2rem; } + + a { color: #0066cc; text-decoration: none; } + a:hover { text-decoration: underline; } + + blockquote { + margin: 1.5rem 0; + padding: 0.8rem 1.2rem; + border-left: 4px solid #0066cc; + background: #f4f8ff; + color: #333; + } + blockquote p { margin: 0; } + + blockquote.example { + border-left: 4px solid #6366f1; + background: #f8f8fb; + padding: 0.8rem 1.2rem 0.2rem; + } + blockquote.example .example-label { + display: inline-block; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #6366f1; + margin-bottom: 0.5rem; + } + + blockquote.schema { + border-left: 4px solid #6366f1; + background: #f8f8fb; + padding: 0.5rem 1.2rem; + margin-bottom: 0; + border-bottom: none; + border-radius: 6px 6px 0 0; + } + blockquote.schema p { margin: 0; font-size: 0.85rem; font-weight: 600; color: #6366f1; } + blockquote.schema + table { + border: 1px solid #c7c7f0; + border-top: none; + border-radius: 0 0 6px 6px; + overflow: hidden; + margin-top: 0; + } + blockquote.schema + table th { + background: #ebebfa; + border-color: #c7c7f0; + } + blockquote.schema + table td { + border-color: #c7c7f0; + } + + code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; + font-size: 0.88em; + background: #f3f3f3; + padding: 0.15em 0.4em; + border-radius: 3px; + } + + pre { + background: #f6f6f7; + color: #1a1a1a; + border: 1px solid #e4e4e7; + padding: 1.2rem 1.4rem; + border-radius: 6px; + overflow-x: auto; + line-height: 1.5; + } + pre code { + background: none; + padding: 0; + font-size: 0.85em; + color: inherit; + } + + table { + border-collapse: collapse; + width: 100%; + margin: 1.2rem 0; + font-size: 0.93em; + display: block; + overflow-x: auto; + } + th { + background: #f0f0f0; + text-align: left; + padding: 0.5rem 0.8rem; + border: 1px solid #d0d0d0; + } + td { + padding: 0.45rem 0.8rem; + border: 1px solid #d0d0d0; + } + tr:nth-child(even) td { background: #fafafa; } + + hr { + border: none; + border-top: 1px solid #e8e8e8; + margin: 2.5rem 0; + } + + ul, ol { padding-left: 1.5rem; } + li { margin: 0.3rem 0; } + + p { margin: 0.8rem 0; } + + em { color: #444; } + + .badge { + display: inline-block; + background: #e4e4e7; + color: #3f3f46; + font-size: 0.78em; + font-weight: 700; + padding: 0.1em 0.45em; + border-radius: 5px; + vertical-align: baseline; + letter-spacing: 0.01em; + } + + .site-header { + background: #fff; + border-bottom: 1px solid #e4e4e7; + position: sticky; + top: 0; + z-index: 100; + } + .site-header-inner { + max-width: 860px; + margin: 0 auto; + padding: 0.6rem 2rem; + display: flex; + align-items: center; + gap: 1.5rem; + } + .site-header a.header-btn { + display: inline-block; + background: #6366f1; + color: #fff; + font-size: 0.9rem; + font-weight: 700; + padding: 0.3em 0.75em; + border-radius: 6px; + text-decoration: none; + transition: background 0.15s; + white-space: nowrap; + } + .site-header a.header-btn:hover { background: #4f46e5; text-decoration: none; } + .site-header .header-tagline { + color: #71717a; + font-size: 0.85rem; + flex: 1; + } + .site-header nav { display: flex; gap: 1.2rem; } + .site-header nav a { + color: #3f3f46; + font-size: 0.85rem; + font-weight: 500; + text-decoration: none; + } + .site-header nav a:hover { color: #6366f1; text-decoration: none; } + + @media (max-width: 600px) { + .site-header-inner { padding: 0.6rem 1rem; gap: 0.8rem; } + .site-header .header-tagline { display: none; } + .site-header nav { gap: 0.8rem; } + } +""" + +HEADER = """ + +""" + + +def process(body: str, title_link: str | None = None, title_text: str = "ifc-commit") -> str: + """Apply post-processing transformations to rendered HTML body.""" + + # Style blockquotes that open with Example + body = re.sub( + r'
\s*

Example', + '

Example', + body, + ) + + # Style blockquotes that open with Schema + body = re.sub( + r'

\s*

Schema', + '

Schema', + body, + ) + + # Protect headings from badge replacement + body = re.sub( + r'(]*>)(.*?)()', + lambda m: m.group(1) + m.group(2).replace('ifc-commit', '\x00') + m.group(3), + body, flags=re.DOTALL, + ) + # Badge-wrap plain-text occurrences of "ifc-commit" + body = re.sub(r'(?ifc-commit', body) + # Restore headings + body = body.replace('\x00', 'ifc-commit') + + # Turn the h1 into a title-button link + if title_link: + body = body.replace( + f'

{title_text}

', + f'

{title_text}

', + 1, + ) + + return body + + +def build(source: Path, output: Path, title: str, title_link: str | None = None, title_text: str = "ifc-commit"): + body = md.render(source.read_text()) + body = process(body, title_link=title_link, title_text=title_text) + + html = f""" + + + + + {title} + + + + {HEADER} +
+ {body} +
+ + +""" + output.write_text(html) + print(f"Written: {output}") + + +build( + source=ROOT / "README.md", + output=ROOT / "index.html", + title="ifc-commit — Version your BIM", + title_link="/", +) + +build( + source=ROOT / "docs" / "research.md", + output=ROOT / "research.html", + title="ifc-commit — Research Notes", + title_link="/research", + title_text="Research Notes", +) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7c4eae1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,2 @@ +# ifc-commit + diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..08828ea --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,124 @@ +# Architecture + +## Overview + +`ifccommit.py` is a single-file CLI tool for slicing and composing IFC files. +It dispatches to one of twelve commands via `argparse` subparsers. + +``` +main() +├── cmd_list — inspect +├── cmd_info — inspect +├── cmd_extract — extract → one output file +├── cmd_insert — merge → one output file +├── cmd_replace — remove space + merge part → one output file +├── cmd_split — extract → one output file per storey +├── cmd_space — extract space + contents → one output file +├── cmd_move — translate element → one output file +├── cmd_copy — copy elements and translate → one output file +├── cmd_diff — diff two IFC files → stdout or file +├── cmd_remove — remove elements by type → one output file +├── cmd_history — write or read Pset_GitCommit on IFC elements +└── cmd_run — fetch and execute yaml pipeline from demo repo +``` + +## Commands + +| Command | Input(s) | Output | Description | +|-----------|-----------------------------------|-----------------|----------------------------------------------------| +| `list` | `input.ifc` | stdout | Count of every IFC type in the file | +| `info` | `input.ifc` + type | stdout | Attributes of every element of that type | +| `extract` | `input.ifc` | `output.ifc` | Extract types/presets into a new file | +| `insert` | `base.ifc` + `part.ifc` | `output.ifc` | Merge part into base | +| `replace` | `base.ifc` + space + `part.ifc` | `output.ifc` | Remove space from base, merge part in its place | +| `split` | `input.ifc` | `outdir/*.ifc` | One file per storey, optional type filter | +| `space` | `input.ifc` + space name | `output.ifc` | Extract a space and all its contained elements | +| `move` | `input.ifc` + entity | `output.ifc` | Translate an element by (x, y, z) metres | +| `copy` | `input.ifc` + ids/tags | `output.ifc` | Copy elements and translate by (x, y, z) metres | +| `diff` | `source.ifc` + `target.ifc` | stdout or file | Show differences between two IFC files | +| `remove` | `input.ifc` + type | `output.ifc` | Remove all elements of a given type | +| `history` | `--input` / `--write-psets` | JSON or stamps | Write or read `Pset_GitCommit` on IFC elements | +| `run` | - | - | Browse and execute yaml pipelines from demo repo | + +## Presets + +`PRESETS` maps short names to lists of IFC types: + +| Preset | Expands to | +|---------------|-----------------------------------------| +| `walls` | `IfcWall`, `IfcWallStandardCase` | +| `storey` | `IfcBuildingStorey` | +| `furnitures` | `IfcFurnishingElement` | + +`resolve_types()` expands preset tokens before building ifcpatch queries. +Raw `IfcType` tokens pass through unchanged, so presets and types can be mixed: + +``` +uv run ifccommit.py extract input.ifc out.ifc walls IfcSlab +``` + +## ifcpatch recipes + +Two recipes are used: + +**`ExtractElements`** — filters elements by an ifcopenshell selector query and +preserves the full spatial hierarchy (site → building → storey) and shared +assets (materials, profiles, styles). The query syntax is: + +``` +IfcWall # all walls (includes subtypes) +IfcWall, location = "Level 1" # walls contained in a named storey +``` + +**`MergeProjects`** — merges one or more `ifcopenshell.file` objects into a +base model. Used by `insert`, `replace`, and `split` when multiple types are +requested (since the `+` multi-type operator breaks the `location` filter — +each type must be extracted separately then merged). + +## Known quirks + +- **Passing file paths to `MergeProjects`** causes a segfault in the current + ifcopenshell version. Always pass `ifcopenshell.file` objects. ([ifcopenshell.file](https://docs.ifcopenshell.org/autoapi/ifcopenshell/file/index.html)) +- **`IfcType + IfcType, location = "X"`** — the `+` operator silently ignores + the location filter. `split` works around this by extracting each type to a + temp file then merging with `MergeProjects`. +- **`IfcWall` covers subtypes** — querying `IfcWall` returns + `IfcWallStandardCase` elements too (IFC inheritance), so the `walls` preset + produces duplicates when both types are extracted with separate location + queries and merged. + +## Output convention + +Extracted files go in `dist/` (git-ignored). Naming: + +| Command | Output path | +|----------------|---------------------------------------------------------| +| `extract` | user-specified | +| `insert` | user-specified | +| `replace` | user-specified | +| `split` | `/_.ifc` | +| `space` | user-specified (or `_.ifc` for multi-match) | +| `move` | user-specified | +| `copy` | user-specified | +| `diff` | stdout (or user-specified with `-o`) | +| `remove` | user-specified (or `__removed.ifc`) | +| `history` | JSON file (read mode) or stamps elements (write mode) | +| `run` | (executes yaml pipeline in place) | + +## Makefile targets + +| Target | Description | +|-------------------|------------------------------------------------------| +| `web` | Start the web app (production entry point) | +| `web-dev` | Start uvicorn with auto-reload on port 8095 | +| `kill` | Kill the running uvicorn process | +| `install-service` | Install systemd service and nginx config | +| `start` | `sudo systemctl start ifc-commit` | +| `stop` | `sudo systemctl stop ifc-commit` | +| `restart` | Rebuild and restart the service | +| `status` | Tail the systemd journal | +| `build` | Build static HTML (`build_html.py`) | +| `release` | Sync source to release repo (`scripts/release.sh`) | +| `clone-demo` | Clone the demo repo locally | +| `reset-git` | Reset the demo repo to a clean state | +| `clean` | Remove `dist/` | diff --git a/docs/commands.md b/docs/commands.md new file mode 100644 index 0000000..f9cb2c1 --- /dev/null +++ b/docs/commands.md @@ -0,0 +1,248 @@ +# Commands + +```bash +uv run ifccommit.py [options] +``` + +## List + +List all IFC types present in a file and their element counts. + +```bash +uv run ifccommit.py list +``` + +**Example:** +```bash +uv run ifccommit.py list model.ifc +``` + +--- + +## Info + +Print attributes of each element matching a given IFC type. + +```bash +uv run ifccommit.py info +``` + +**Example:** +```bash +uv run ifccommit.py info model.ifc IfcWall +``` + +--- + +## Extract + +Extract elements of given IFC types (or presets) into a new IFC file. + +```bash +uv run ifccommit.py extract [...] +``` + +**Presets:** +- `walls` — IfcWall, IfcWallStandardCase +- `storey` — IfcBuildingStorey +- `furnitures` — IfcFurnishingElement + +**Examples:** +```bash +uv run ifccommit.py extract model.ifc walls.ifc walls +uv run ifccommit.py extract model.ifc slabs.ifc IfcSlab +``` + +--- + +## Insert + +Insert (merge) a part IFC file into a base IFC file. + +```bash +uv run ifccommit.py insert +``` + +**Example:** +```bash +uv run ifccommit.py insert base.ifc part.ifc merged.ifc +``` + +--- + +## Replace + +Remove a space and its contents from base, then merge the part file in its place. + +```bash +uv run ifccommit.py replace +``` + +**Example:** +```bash +uv run ifccommit.py replace model.ifc A102 part.ifc result.ifc +``` + +--- + +## Split + +Split an IFC file into one file per storey, optionally filtered by type. + +```bash +uv run ifccommit.py split [IfcType|preset ...] +``` + +**Examples:** +```bash +uv run ifccommit.py split model.ifc output/ +uv run ifccommit.py split model.ifc output/ walls +``` + +--- + +## Space + +Extract an IfcSpace and all objects contained within it. + +```bash +uv run ifccommit.py space [--by name|longname] +``` + +**Options:** +- `--by name` — Match against Name (default) +- `--by longname` — Match against LongName + +**Examples:** +```bash +uv run ifccommit.py space model.ifc output.ifc A102 +uv run ifccommit.py space model.ifc output.ifc "Living Room" --by longname +``` + +--- + +## Move + +Translate an element by (x, y, z) metres and write to a new file. + +```bash +uv run ifccommit.py move [entity_id] [--name X] [--x N] [--y N] [--z N] +``` + +**Options:** +- `entity_id` — IFC entity id (e.g. 17902) +- `--name` — Find element by name substring +- `--x`, `--y`, `--z` — Offset in metres (default: 0.0) + +**Examples:** +```bash +uv run ifccommit.py move model.ifc output.ifc 17902 --x 5.0 --z 1.5 +uv run ifccommit.py move model.ifc output.ifc --name "Door" --x 2.0 +``` + +--- + +## Copy + +Copy elements by entity ids or tags and translate copies by (x, y, z) metres. + +```bash +uv run ifccommit.py copy [--entity-ids N ...] [--tags X ...] [--x N] [--y N] [--z N] +``` + +**Options:** +- `--entity-ids` — IFC entity ids to copy +- `--tags` — IFC element tags to copy +- `--x`, `--y`, `--z` — Offset in metres (default: 0.0) + +**Examples:** +```bash +uv run ifccommit.py copy model.ifc output.ifc --entity-ids 17902 17903 --x 5.0 +uv run ifccommit.py copy model.ifc output.ifc --tags ABC123 --y 10.0 +``` + +--- + +## Diff + +Show differences between two IFC files. + +```bash +uv run ifccommit.py diff [-v] [-o FILE] +``` + +**Options:** +- `-v`, `--verbose` — Show detailed changes (added, removed, modified elements) +- `-o`, `--output` — Write results to output file + +**Examples:** +```bash +uv run ifccommit.py diff old.ifc new.ifc +uv run ifccommit.py diff old.ifc new.ifc -v -o diff.txt +``` + +--- + +## History + +Write or read `Pset_GitCommit` properties on IFC elements. + +**Write mode** — stamp `Pset_GitCommit` on all output elements using `HEAD`: + +```bash +uv run ifccommit.py history --write-psets [--workdir DIR] [--yaml YAML] +``` + +**Read mode** — read `Pset_GitCommit` back and emit JSON: + +```bash +uv run ifccommit.py history --input [--output ] +``` + +**Options:** +- `--write-psets` — Enable write mode; stamp elements with current `HEAD` +- `--input` — IFC file to read `Pset_GitCommit` from (read mode) +- `--output` — Output JSON file (read mode) +- `--workdir` — Git repository root (default: cwd) +- `--yaml` — Repo-relative path to the pipeline yaml (default: `yaml/duplex.yaml`) + +**Examples:** +```bash +uv run ifccommit.py history --write-psets --yaml yaml/my-pipeline.yaml +uv run ifccommit.py history --input result.ifc --output history.json +``` + +--- + +## Remove + +Remove all elements contained within each instance of an IFC type. + +```bash +uv run ifccommit.py remove [--output FILE] +``` + +**Options:** +- `--output` — Output IFC file (default: `__removed.ifc`) + +**Example:** +```bash +uv run ifccommit.py remove model.ifc IfcSpace --output cleaned.ifc +``` + +--- + +## Run + +Browse and run a pipeline yaml from the demo repo. + +```bash +uv run ifccommit.py run +``` + +This command fetches available pipeline YAML files from the demo repository, lets you select one, and executes it locally. + +**Example:** +```bash +uv run ifccommit.py run +``` \ No newline at end of file diff --git a/docs/history.md b/docs/history.md new file mode 100644 index 0000000..2fe8bae --- /dev/null +++ b/docs/history.md @@ -0,0 +1,137 @@ +# Embedding Commit History in IFC Files + +A survey of IFC mechanisms for storing git commit metadata. + +--- + +## 1. IfcOwnerHistory — The Native Mechanism + +Every `IfcRoot`-derived entity (walls, spaces, products, etc.) carries an optional `IfcOwnerHistory` attribute. It is the closest thing IFC has to built-in change tracking. + +**Fields:** + +| Field | Type | Notes | +|-------|------|-------| +| `OwningUser` | `IfcPersonAndOrganization` | Who created the element | +| `OwningApplication` | `IfcApplication` | Software used | +| `State` | `IfcStateEnum` | `READWRITE`, `READONLY`, `LOCKED` | +| `ChangeAction` | `IfcChangeActionEnum` | `ADDED`, `MODIFIED`, `DELETED`, `NOCHANGE` | +| `LastModifiedDate` | `IfcTimeStamp` | Unix timestamp | +| `LastModifyingUser` | `IfcPersonAndOrganization` | | +| `LastModifyingApplication` | `IfcApplication` | | +| `CreationDate` | `IfcTimeStamp` | Unix timestamp | + +Raw IFC line from `samples/duplex.ifc`: +``` +#33=IFCOWNERHISTORY(#32,#2,$,.NOCHANGE.,$,$,$,0); +``` + +**Limitations:** + +- Only the *current* state — no history chain +- `ChangeAction` is a coarse enum; no room for a commit hash, message, or branch +- `IfcApplication.Version` is a short string, not designed for structured data +- One record per element; previous owners are lost on update + +**Verdict:** Good for standard compliance and timestamping. Not sufficient alone for git metadata. + +--- + +## 2. IfcPropertySet — The Recommended Extension Point + +Custom property sets (`Pset_*`) are the standard IFC way to attach arbitrary key-value metadata to any `IfcObject`. They survive round-trips through most IFC-aware tools (unknown Psets are ignored, not discarded). + +**Proposed schema — `Pset_GitCommit`:** + +| Property | Type | Example | +|----------|------|---------| +| `CommitHash` | `IfcLabel` | `a1b2c3d4f5e6c7b8` | +| `CommitMessage` | `IfcText` | `Fix wall thickness` | +| `CommitAuthor` | `IfcLabel` | `alice ` | +| `CommitDate` | `IfcLabel` | `2026-03-24T14:30:00Z` | +| `CommitBranch` | `IfcLabel` | `main` | +| `OperationName` | `IfcLabel` | `Modify` | + +**ifcopenshell snippet:** + +```python +pset = ifcopenshell.api.pset.add_pset(model, product=element, name="Pset_GitCommit") +ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={ + "CommitHash": commit_hash, + "CommitMessage": commit_message, + "CommitAuthor": commit_author, + "CommitDate": commit_date, + "CommitBranch": branch, +}) +``` + +**Reading back:** + +```python +for rel in element.IsDefinedBy or []: + if rel.is_a("IfcRelDefinesByProperties"): + pset = rel.RelatingPropertyDefinition + if pset.Name == "Pset_GitCommit": + props = {p.Name: p.NominalValue.wrappedValue for p in pset.HasProperties} +``` + +**Verdict:** Best fit for per-element traceability. Flexible, queryable, spec-compliant. + +--- + +## 3. IfcDocumentInformation — For Linking to External Commits + +`IfcDocumentInformation` + `IfcRelAssociatesDocument` lets you attach a document reference (URL, identifier, description) to any `IfcRoot` entity. It is designed for spec sheets and drawings but can carry a git commit URL. + +```python +doc = ifcopenshell.api.document.add_information(model) +ifcopenshell.api.document.edit_information(model, information=doc, attributes={ + "Identification": commit_hash[:8], + "Name": commit_message, + "Location": f"https://gitaec.org/rvba/ifc-commit/commit/{commit_hash}", +}) +ref = ifcopenshell.api.document.add_reference(model, information=doc) +ifcopenshell.api.document.assign_document(model, products=[element], document=ref) +``` + +**Verdict:** Useful if you want to link elements back to a hosted commit URL. More verbose than a Pset. Better for file-level "source revision" than per-element tracking. + +--- + +## 4. IfcApplication — File-Level Commit Stamp + +`IfcApplication` is referenced by every `IfcOwnerHistory`. Its `Version` field can carry the current commit hash as a lightweight file-level stamp. + +```python +app = ifcopenshell.api.owner.add_application(model, ...) +ifcopenshell.api.owner.edit_application(model, application=app, attributes={ + "ApplicationIdentifier": "ifc-commit", + "Version": commit_hash, # e.g. "a1b2c3d4" + "Name": "ifc-commit", +}) +``` + +**Verdict:** Zero overhead, but limited to one hash per file. Good as a quick "what commit produced this file" marker. + +--- + +## 5. Comparison + +| Mechanism | Granularity | Stores hash/message | IFC compliance | Overhead | +|-----------|-------------|---------------------|----------------|----------| +| `IfcOwnerHistory` | per-element | No | Native | Minimal | +| `Pset_GitCommit` | per-element | Yes (all fields) | Standard extension | Medium | +| `IfcDocumentInformation` | per-element | Yes (via Location) | Standard | High | +| `IfcApplication.Version` | per-file | Hash only | Native | Minimal | + +--- + +## 6. Recommendation + +A two-layer approach: + +1. **File level** — Set `IfcApplication.Version` to the commit hash. Every tool that reads `IfcOwnerHistory` will expose this. + +2. **Element level** — On elements touched by an operation (extract, merge, replace), write a `Pset_GitCommit` property set with the full commit metadata. Update `IfcOwnerHistory.ChangeAction` to `ADDED` or `MODIFIED` accordingly. + +This keeps standard IFC compliance intact while making full git provenance queryable directly from the model. diff --git a/docs/ifc-extract.md b/docs/ifc-extract.md new file mode 100644 index 0000000..8347abc --- /dev/null +++ b/docs/ifc-extract.md @@ -0,0 +1,133 @@ +# IFC Extraction — Research Notes + +How to extract parts from an IFC file, for use in the ifc-commit pipeline. + +--- + +## 1. Core Tools + +### IfcOpenShell (Python) + +The main toolkit. Install via pip: + +```bash +pip install ifcopenshell +``` + +Open a file and select elements by type: + +```python +import ifcopenshell + +model = ifcopenshell.open("model.ifc") + +# All products (geometric elements) +products = model.by_type("IfcProduct") + +# Specific type +walls = model.by_type("IfcWall") + +# Inspect attributes +for wall in walls: + print(wall.get_info()) +``` + +--- + +## 2. Extract to a New IFC File + +### `ifcpatch` — `ExtractElements` recipe + +The cleanest high-level API for extracting a subset of elements into a new file: + +```python +import ifcopenshell +import ifcpatch + +model = ifcopenshell.open("input.ifc") + +result = ifcpatch.execute({ + "input": "input.ifc", + "file": model, + "recipe": "ExtractElements", + "arguments": ["IfcWall"] # or "IfcWall, IfcSlab" for multiple types +}) + +ifcpatch.write(result, "walls-only.ifc") +``` + +Key parameters: +| Parameter | Default | Description | +|-----------|---------|-------------| +| `query` | `"IfcWall"` | IFC type(s) to extract, comma-separated | +| `assume_asset_uniqueness_by_name` | `True` | Deduplicate materials/profiles by name | + +The recipe preserves spatial hierarchy (`add_spatial_structures`) and handles assets (materials, styles, profiles) via `append_asset`. + +### `ifcopenshell.file.add()` — Low-level copy + +Copies an entity and all its dependencies recursively into another file: + +```python +import ifcopenshell + +source = ifcopenshell.open("source.ifc") +target = ifcopenshell.file(schema=source.schema) + +for wall in source.by_type("IfcWall"): + target.add(wall) # recursive deep copy + +target.write("extracted.ifc") +``` + +Note: produces a valid-ish file but may need cleanup (missing spatial structure, duplicate references). + +--- + +## 3. Extract by GlobalId (CLI) + +Using `IfcConvert` to filter by specific element IDs and export to another format: + +```bash +IfcConvert model.ifc out.glb \ + --include attribute GlobalId 1yETHMphv6LwABqR4Pbs5g \ + --include attribute GlobalId 2xETHMphv6LwABqR0Pbs5g +``` + +--- + +## 4. Spatial Decomposition + +To get all sub-elements of a spatial element (building, storey, space…): + +```python +from ifcopenshell.util.element import get_decomposition + +storey = model.by_type("IfcBuildingStorey")[0] +elements = get_decomposition(storey) +# → all elements contained in that storey, their parts, openings, fills +``` + +--- + +## 5. Relevance for ifc-commit + +| Operation | Tool | Notes | +|-----------|------|-------| +| Extract by type | `ifcpatch ExtractElements` | cleanest API | +| Extract by ID | `IfcConvert --include` | CLI, good for scripting | +| Deep copy to new file | `ifcopenshell.file.add()` | low-level, needs post-processing | +| Traverse spatial tree | `get_decomposition()` | useful for bus routing logic | + +Next step: **insert** extracted parts into a target IFC file → see `ifc-insert.md`. + +--- + +## Sources + +- [IfcOpenShell — parse IFC files with Python](https://thinkmoult.com/using-ifcopenshell-parse-ifc-files-python.html) +- [ifcpatch ExtractElements recipe](https://docs.ifcopenshell.org/autoapi/ifcpatch/recipes/ExtractElements/index.html) +- [IfcOpenShell code examples — OSArch Wiki](https://wiki.osarch.org/index.php?title=IfcOpenShell_code_examples) +- [Splitting an IFC file into parts — SourceForge discussion](https://sourceforge.net/p/ifcopenshell/discussion/1782718/thread/1290d16904/) +- [Extracting entities from IFC — SourceForge discussion](https://sourceforge.net/p/ifcopenshell/discussion/1782717/thread/72b5297e51/) +- [IfcOpenShell code examples](https://docs.ifcopenshell.org/ifcopenshell-python/code_examples.html) diff --git a/docs/ifcfile.md b/docs/ifcfile.md new file mode 100644 index 0000000..eed5d7a --- /dev/null +++ b/docs/ifcfile.md @@ -0,0 +1,160 @@ +# `ifcopenshell.file` + +An `ifcopenshell.file` is the in-memory representation of a parsed IFC file. +It is the central object in every ifcbus operation — all reading, querying, +modifying, and writing go through it. + +--- + +## Opening a file + +```python +import ifcopenshell + +model = ifcopenshell.open("input.ifc") +# model is an ifcopenshell.file object +``` + +`ifcopenshell.open()` parses the STEP-encoded `.ifc` file and loads all +entities into memory. The result is a live, mutable object graph. + +--- + +## What it contains + +An IFC file is a flat list of **entity instances**, each identified by a +sequential integer id (`#1`, `#2`, …). Entities reference each other by id, +forming a directed graph. + +``` +#1 = IfcOrganization(...) +#2 = IfcApplication(#1, ...) +#33 = IfcOwnerHistory(#32, #2, ...) +#514 = IfcSpace('0BTBFw6f90Nfh9rP1dlXrr', #33, 'A101', ...) +``` + +The `ifcopenshell.file` object holds: +- **All entity instances** (the flat list above) +- **Schema** — e.g. `IFC2X3`, `IFC4` +- **Header** — file metadata (author, application, description, timestamp) + +--- + +## Key methods + +### Querying + +```python +# By entity id (integer) +el = model.by_id(514) + +# By GlobalId (GUID string) +el = model.by_guid("0BTBFw6f90Nfh9rP1dlXrr") + +# All instances of a type (includes subtypes) +walls = model.by_type("IfcWall") # returns list +spaces = model.by_type("IfcSpace") + +# Iterate over every entity in the file +for entity in model: + print(entity.id(), entity.is_a()) +``` + +### Relationships + +```python +# All entities that reference this element (inverse links) +refs = model.get_inverse(el) # returns set of entity instances + +# All entities reachable from this element (deep graph traversal) +graph = model.traverse(el) # returns list, breadth-first +``` + +### Modifying + +```python +# Create a new entity +org = model.create_entity("IfcOrganization", Name="ACME") + +# Remove an entity and clean up its relationships +model.remove(el) + +# Higher-level removal (cleans containment, placements, etc.) +import ifcopenshell.api +ifcopenshell.api.run("root.remove_product", model, product=el) +``` + +### Writing + +```python +# Write back to a file +model.write("output.ifc") + +# Or via ifcpatch (used when the result comes from a recipe) +import ifcpatch +ifcpatch.write(result, "output.ifc") +``` + +--- + +## Schema and header + +```python +print(model.schema) # "IFC2X3" or "IFC4" +print(model.header.file_description) # ViewDefinition, ... +print(model.header.file_name.name) # original filename +``` + +--- + +## Entity instances + +Each element returned by the methods above is an **entity instance** +(`ifcopenshell.entity_instance`). Its attributes are accessed as properties: + +```python +wall = model.by_type("IfcWall")[0] + +wall.GlobalId # GUID string +wall.Name # element name +wall.is_a() # "IfcWall" — runtime type string +wall.id() # integer entity id (file-local, unstable across extractions) +wall.get_info() # dict of all attribute name → value +``` + +Entity ids are **not stable** — the same physical element will have a different +id after extraction or merging. Always use `GlobalId` or `Name` to identify +elements across files. + +--- + +## Relationship to ifcpatch + +`ifcpatch.execute()` takes an `ifcopenshell.file` as input and returns a new +one as output. The input is never modified in place. `ifcpatch.write()` is a +thin wrapper around `model.write()` with some post-processing. + +```python +result = ifcpatch.execute({ + "input": "input.ifc", + "file": model, # ← ifcopenshell.file + "recipe": "ExtractElements", + "arguments": ["IfcWall"], +}) +# result is a new ifcopenshell.file +ifcpatch.write(result, "output.ifc") +``` + +--- + +## Memory model + +The entire file is loaded into memory as a C++ object graph (via a Python +binding). This means: + +- Operations are fast (no re-parsing on each query). +- Large files consume significant RAM. +- The object is **not thread-safe** — do not share across threads. +- Passing the object to `MergeProjects` as a file path string (instead of the + object itself) triggers a segfault in the current version — always pass the + live object. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..eb8ab49 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,64 @@ +# Installation + +## Requirements + +- Python 3.11+ +- [uv](https://docs.astral.sh/uv/) +- nginx +- systemd + +## Clone + +```bash +git clone https://gitaec.org/rvba/ifc-commit +cd ifc-commit +``` + +## Install dependencies + +```bash +uv sync +``` + +## Development server + +```bash +make web-dev +``` + +Starts uvicorn on `http://127.0.0.1:8095` with auto-reload. + +## Production deployment + +### 1. Install the systemd service and nginx config + +```bash +make install-service +``` + +This will: +- Write `/etc/systemd/system/ifc-commit.service` (filled with the current working directory and user) +- Symlink `files/ifc-commit.conf` into `/etc/nginx/sites-enabled/` +- Enable and start the `ifc-commit` systemd service +- Reload nginx + +### 2. Configure your domain + +Edit `files/ifc-commit.conf` and set `server_name` to your domain before running `make install-service`. + +### 3. Manage the service + +```bash +make start # sudo systemctl start ifc-commit +make stop # sudo systemctl stop ifc-commit +``` + +Check logs: + +```bash +journalctl -u ifc-commit -f +``` + +## Personal access token + +The web UI has a **Personal access token** field. The token is saved in the browser's `localStorage` and never stored server-side. Generate one from your Forgejo account settings. diff --git a/docs/merge.md b/docs/merge.md new file mode 100644 index 0000000..a2da754 --- /dev/null +++ b/docs/merge.md @@ -0,0 +1,132 @@ +# IFC Merging + +How IFC files are merged in the ifc-commit pipeline. + +--- + +## Recipe: `MergeProjects` + +All merging in ifccommit uses the `ifcpatch` recipe `MergeProjects`. It combines +two or more `ifcopenshell.file` objects into a single model: + +```python +import ifcopenshell +import ifcpatch + +base = ifcopenshell.open("base.ifc") +part = ifcopenshell.open("part.ifc") + +result = ifcpatch.execute({ + "input": "base.ifc", + "file": base, + "recipe": "MergeProjects", + "arguments": [[part]], # list of ifcopenshell.file objects +}) + +ifcpatch.write(result, "merged.ifc") +``` + +**Important**: always pass `ifcopenshell.file` objects, not file paths. +Passing a path string causes a segfault in the current ifcopenshell version. + +--- + +## What MergeProjects does + +1. **Combines `IfcProject` elements** — the two projects are merged into one. +2. **Appends all entities** from the part model into the base model, remapping + entity ids to avoid collisions. +3. **Converts length units** — the part is automatically rescaled to match the + base model's unit before merging. +4. **Does not deduplicate** spatial hierarchies — if both files have a site, + building, and storey, the result will contain two of each. Use `replace` + instead of `insert` when updating an existing space. + +--- + +## `insert` vs `replace` + +| Command | Behaviour | +|-----------|-----------| +| `insert` | Appends part into base — no removal. May duplicate spatial elements. | +| `replace` | Removes the target space first, then merges part in. Clean result. | + +### `insert` + +``` +base.ifc (295 products) ++ part.ifc (9 products) += merged.ifc (304 products) ← space objects duplicated +``` + +Used when the part introduces new elements that do not already exist in the base. + +### `replace` + +``` +base.ifc (295 products) +− space A102 + contents (6 elements removed) ++ modified_space.ifc (9 products) += merged.ifc (298 products) ← clean replacement +``` + +Used when updating a space that already exists in the base model. + +--- + +## Internals of `replace` + +`cmd_replace` in ifccommit.py performs the removal step manually before merging: + +```python +# 1. Find and remove the space and its contained elements +contained = ifcopenshell.util.selector.filter_elements( + model, f'IfcElement, location = "{space_name}"' +) +for el in list(contained) + [space]: + ifcopenshell.api.run("root.remove_product", model, product=el) + +# 2. Write stripped base to a temp file +model.write(tmp) + +# 3. Merge the part in +stripped = ifcopenshell.open(tmp) +part_model = ifcopenshell.open(part_path) +result = ifcpatch.execute({ + "input": tmp, "file": stripped, + "recipe": "MergeProjects", + "arguments": [[part_model]], +}) +ifcpatch.write(result, output) +``` + +`root.remove_product` cleans up the element and its direct relationships +(placements, containment links). Geometry and shared assets are handled by the +model's internal garbage collection on write. + +--- + +## Multi-type merging in `split` + +The `location` filter in ifcopenshell's selector breaks when combined with the +`+` operator for multiple types (`IfcWall + IfcSlab, location = "Level 1"` +silently drops the filter). The workaround used in `cmd_split`: + +1. Extract each type separately with its own location-filtered query. +2. Write each extraction to a temp file. +3. Merge all temp files with `MergeProjects`. +4. Clean up temp files. + +This is encapsulated in the `extract_with_location()` helper in `ifccommit.py`. + +--- + +## Known quirks + +- **Segfault with path strings** — `MergeProjects` crashes if given a file path + instead of an `ifcopenshell.file` object. Always open the file first. +- **Duplicate spatial hierarchy** — `insert` does not merge sites, buildings, or + storeys; use `replace` for clean substitution. +- **Aggregated vs contained spaces** — the `location` filter only follows + `IfcRelContainedInSpatialStructure`, not `IfcRelAggregates`. Spaces linked via + aggregation appear empty to the filter (e.g. space B104 in duplex.ifc). diff --git a/docs/merge_native.md b/docs/merge_native.md new file mode 100644 index 0000000..66b3585 --- /dev/null +++ b/docs/merge_native.md @@ -0,0 +1,152 @@ +# Native IFC Merging with ifcmerge + +How [ifcmerge](https://github.com/brunopostle/ifcmerge) relates to the ifc-commit +pipeline, and when to use it instead of (or alongside) `ifcpatch MergeProjects`. + +--- + +## What is ifcmerge? + +ifcmerge is a **three-way merge tool for IFC/STEP files**, written by Bruno +Postle (GPLv3, 2022). It brings software-development-style branching workflows +to BIM files stored in git. + +Basic usage: + +```bash +ifcmerge base.ifc local_fork.ifc remote_fork.ifc merged.ifc +``` + +It can be registered as a git mergetool: + +```bash +git config --global mergetool.ifcmerge.cmd \ + 'ifcmerge $BASE $LOCAL $REMOTE $MERGED' +git config --global mergetool.ifcmerge.trustExitCode true +``` + +Then on a merge conflict: + +```bash +git merge feature-branch +git mergetool --tool=ifcmerge +``` + +--- + +## How it works + +IFC files are encoded in the STEP Physical File (SPF) format. Each entity has a +numeric id (`#514`, `#819`, …). ifcmerge uses these ids as stable keys — not +line numbers — to track changes between versions. + +Given three versions of the **same model**: + +1. **Analysis** — identifies which entity ids were added, deleted, or modified + in each branch relative to the common ancestor. +2. **ID renumbering** — newly-added entities from one branch are renumbered to + avoid collisions with the other branch. +3. **Attribute-level merge** — if branch A changed `Name` and branch B changed + `Representation` on the same entity, both changes are applied. No conflict. +4. **List merging** — list-valued attributes (e.g. `IfcRelContainedInSpatialStructure.RelatedElements`) + support independent additions and deletions from each branch. +5. **Conflict detection** — if an entity is **deleted in one branch but modified + in the other**, ifcmerge refuses and exits non-zero. Manual resolution required. + +--- + +## ifcmerge vs `MergeProjects` + +These tools solve **different problems**: + +| Aspect | ifcmerge | ifcpatch `MergeProjects` | +|--------|----------|--------------------------| +| Purpose | Reconcile two divergent edits of the **same** model | Combine two **independent** models | +| Common ancestor | Required | Not applicable | +| Conflict detection | Yes — delete/modify conflicts | None | +| ID handling | Renumbers only new entities | Renumbers all entities from one side | +| Attribute merging | Attribute-by-attribute within same entity | No concept of same-entity merging | +| Git integration | Designed as a git mergetool | No relation to version control | +| Use case | Concurrent editing by multiple contributors | Federation / composing discipline models | + +**In short:** +- Use `MergeProjects` (via `ifccommit insert` or `ifccommit replace`) to **compose** + models — e.g. merging an extracted and modified space back into the original. +- Use `ifcmerge` to **reconcile** concurrent edits — e.g. two contributors + independently edited the same model on separate branches. + +--- + +## Relevance for ifc-commit + +The ifc-commit pipeline operates as: + +``` +extract → modify → replace +``` + +This is a **sequential** workflow: one person extracts a space, modifies it, +and merges it back. ifcmerge adds value when the workflow becomes **concurrent**: + +``` + ┌── contributor A: moves furniture ──┐ +base ┤ ├→ ifcmerge → merged.ifc + └── contributor B: renames spaces ──┘ +``` + +A git-based BIM workflow combining both tools: + +```bash +# Both contributors branch from the same model +git checkout -b feature/move-table +# A edits duplex.ifc (moves table) +git commit -m "move coffee table in A102" + +git checkout main +git checkout -b feature/rename-spaces +# B edits duplex.ifc (renames spaces) +git commit -m "rename spaces to long names" + +# Merge B's changes into A's branch +git checkout feature/move-table +git merge feature/rename-spaces +# conflict on duplex.ifc → resolve with ifcmerge +git mergetool --tool=ifcmerge +git commit -m "merge: move table + rename spaces" +``` + +--- + +## Installation + +ifcmerge is **not a Python package** — it cannot be installed with `uv add`. + +| Platform | Method | +|----------|--------| +| Linux / macOS | Clone repo, put the `ifcmerge` Perl script on `$PATH` | +| Windows | Download pre-built `ifcmerge.exe` from the GitHub releases page | +| Rust port | Experimental binary available in the 2025-01-26 release | + +--- + +## Limitations + +- **Native IFC required** — only works if the editing application writes IFC + with stable entity ids. Applications that regenerate all ids on export + (Revit, ArchiCAD in export mode) are incompatible. +- **Delete/modify conflicts** — cannot auto-resolve; user must abort the merge + and resolve manually. +- **No parametric logic** — IFC captures geometry, not design intent. Two + branches that drive geometry from incompatible parameter sets may produce + geometrically inconsistent results. +- **Experimental Rust port** — the Perl implementation is the stable one. +- **Large file handling** — git forges cap LFS file sizes; large projects + (> 1 GB) need a self-hosted instance. + +--- + +## References + +- [ifcmerge GitHub](https://github.com/brunopostle/ifcmerge) +- [Porto OpenBIM Hackathon 2026](http://porto.gitaec.org) +- [Git-based IFC example by Bruno Postle](https://gitaec.org/brunopostle/creative-freedom) diff --git a/docs/research.md b/docs/research.md new file mode 100644 index 0000000..708edf7 --- /dev/null +++ b/docs/research.md @@ -0,0 +1,226 @@ +# Research Notes + +This document covers the research behind ifc-commit's approach to storing git +provenance inside IFC files: the mechanisms surveyed, the tradeoffs considered, +and how the implementation was designed. + +--- + +## Embedding Commit History in IFC Files + +A survey of IFC mechanisms for storing git commit metadata. + +--- + +### 1. IfcOwnerHistory — The Native Mechanism + +Every `IfcRoot`-derived entity (walls, spaces, products, etc.) carries an optional `IfcOwnerHistory` attribute. It is the closest thing IFC has to built-in change tracking. + +**Fields:** + +| Field | Type | Notes | +|-------|------|-------| +| `OwningUser` | `IfcPersonAndOrganization` | Who created the element | +| `OwningApplication` | `IfcApplication` | Software used | +| `State` | `IfcStateEnum` | `READWRITE`, `READONLY`, `LOCKED` | +| `ChangeAction` | `IfcChangeActionEnum` | `ADDED`, `MODIFIED`, `DELETED`, `NOCHANGE` | +| `LastModifiedDate` | `IfcTimeStamp` | Unix timestamp | +| `LastModifyingUser` | `IfcPersonAndOrganization` | | +| `LastModifyingApplication` | `IfcApplication` | | +| `CreationDate` | `IfcTimeStamp` | Unix timestamp | + +Raw IFC line from `samples/duplex.ifc`: +``` +#33=IFCOWNERHISTORY(#32,#2,$,.NOCHANGE.,$,$,$,0); +``` + +**Limitations:** + +- Only the *current* state — no history chain +- `ChangeAction` is a coarse enum; no room for a commit hash, message, or branch +- `IfcApplication.Version` is a short string, not designed for structured data +- One record per element; previous owners are lost on update + +**Verdict:** Good for standard compliance and timestamping. Not sufficient alone for git metadata. + +--- + +### 2. IfcPropertySet — The Recommended Extension Point + +Custom property sets (`Pset_*`) are the standard IFC way to attach arbitrary key-value metadata to any `IfcObject`. They survive round-trips through most IFC-aware tools (unknown Psets are ignored, not discarded). + +> **Schema** — `Pset_GitCommit` + +| Property | Type | Example | +|----------|------|---------| +| `CommitHash` | `IfcLabel` | `a1b2c3d4f5e6c7b8` | +| `CommitMessage` | `IfcText` | `Fix wall thickness` | +| `CommitAuthor` | `IfcLabel` | `alice <***@***>` | +| `CommitDate` | `IfcLabel` | `2026-03-24T14:30:00Z` | +| `CommitBranch` | `IfcLabel` | `main` | +| `OperationName` | `IfcLabel` | `Merge` | + +**ifcopenshell snippet — writing:** + +```python +pset = ifcopenshell.api.pset.add_pset(model, product=element, name="Pset_GitCommit") +ifcopenshell.api.pset.edit_pset(model, pset=pset, properties={ + "CommitHash": commit_hash, + "CommitMessage": commit_message, + "CommitAuthor": commit_author, + "CommitDate": commit_date, + "CommitBranch": branch, + "OperationName": operation_name, +}) +``` + +**Reading back:** + +```python +for rel in element.IsDefinedBy or []: + if rel.is_a("IfcRelDefinesByProperties"): + pset = rel.RelatingPropertyDefinition + if pset.Name == "Pset_GitCommit": + props = {p.Name: p.NominalValue.wrappedValue for p in pset.HasProperties} +``` + +**Verdict:** Best fit for per-element traceability. Flexible, queryable, spec-compliant. + +--- + +### 3. IfcDocumentInformation — For Linking to External Commits + +`IfcDocumentInformation` + `IfcRelAssociatesDocument` lets you attach a document reference (URL, identifier, description) to any `IfcRoot` entity. It can carry a git commit URL back to the forge. + +```python +doc = ifcopenshell.api.document.add_information(model) +ifcopenshell.api.document.edit_information(model, information=doc, attributes={ + "Identification": commit_hash[:8], + "Name": commit_message, + "Location": f"https://gitaec.org/rvba/ifc-commit/commit/{commit_hash}", +}) +ref = ifcopenshell.api.document.add_reference(model, information=doc) +ifcopenshell.api.document.assign_document(model, products=[element], document=ref) +``` + +**Verdict:** Useful for linking elements to a hosted commit URL. More verbose than a Pset. Better suited for file-level "source revision" than per-element tracking. + +--- + +### 4. IfcApplication — File-Level Commit Stamp + +`IfcApplication` is referenced by every `IfcOwnerHistory`. Its `Version` field can carry the current commit hash as a lightweight file-level stamp. + +```python +app = ifcopenshell.api.owner.add_application(model) +ifcopenshell.api.owner.edit_application(model, application=app, attributes={ + "ApplicationIdentifier": "ifc-commit", + "Version": commit_hash, + "Name": "ifc-commit", +}) +``` + +**Verdict:** Zero overhead. Limited to one hash per file. Good as a quick "what commit produced this file" marker. + +--- + +### 5. Comparison + +| Mechanism | Granularity | Stores hash/message | IFC compliance | Overhead | +|-----------|-------------|---------------------|----------------|----------| +| `IfcOwnerHistory` | per-element | No | Native | Minimal | +| `Pset_GitCommit` | per-element | Yes (all fields) | Standard extension | Medium | +| `IfcDocumentInformation` | per-element | Yes (via Location) | Standard | High | +| `IfcApplication.Version` | per-file | Hash only | Native | Minimal | + +--- + +### 6. Adopted Approach + +A two-layer design: + +1. **File level** — `IfcApplication.Version` is set to the commit hash. Every tool that reads `IfcOwnerHistory` exposes this with no extra work. + +2. **Element level** — On elements touched by an operation, a `Pset_GitCommit` property set is written with the full commit metadata. `IfcOwnerHistory.ChangeAction` is updated to `ADDED` or `MODIFIED` accordingly. + +This keeps standard IFC compliance intact while making full git provenance queryable directly from the model. + +--- + +## Implementation Plan + +The following section describes how the history mechanism is integrated into the pipeline and webapp. + +--- + +### Pipeline Integration + +The `history` command in ifc-commit operates in two modes: + +- **Write mode** (`write_psets: true`): at the end of a pipeline run, stamps `Pset_GitCommit` on all elements in every operation's output IFC, using the current `HEAD` commit. +- **Read mode** (`input` present): opens an IFC file, collects all `Pset_GitCommit` records, and writes them to a JSON file for the webapp. + +**Example pipeline declaration:** + +```yaml +operations: + - name: Extract + command: extract + input: ifc/duplex.ifc + output: ifc/duplex_extract.ifc + type: IfcSpace + id: A102 + + - name: Modify + command: modify + input: ifc/duplex_extract.ifc + output: ifc/duplex_modified.ifc + element: "168381" + x: 2 + + - name: Merge + command: merge + base: ifc/duplex.ifc + space: A102 + part: ifc/duplex_modified.ifc + output: ifc/duplex_merge.ifc + + - name: WriteHistory + command: history + write_psets: true + + - name: ReadHistory + command: history + input: ifc/duplex_merge.ifc + output: ifc/history.json +``` + +--- + +### Webapp Integration + +After a pipeline run, the webapp calls `/api/ifc-history` to surface the per-element commit metadata, with links back to the corresponding commits on the forge. + +Each element that was touched by the pipeline carries: + +> **Schema** — per-element provenance + +| Property | Example | +|----------|---------| +| `CommitHash` | `a1b2c3d4…` | +| `CommitMessage` | `move table` | +| `CommitAuthor` | `rvba <***@***>` | +| `CommitDate` | `2026-03-24T14:30:00Z` | +| `CommitBranch` | `main` | +| `OperationName` | `Modify` | + +The element history panel links each commit hash to its page on the forge, making the full modification trail navigable directly from the webapp. + +--- + +## Related Work + +- [ifc-data-bus](https://github.com/vyzn-tech/ifc-data-bus) +- [ifc-data-horse](https://gitaec.org/rvba/ifc-data-horse) +- [buildingSMART Hackathon 2026](https://github.com/lfcastel/buildingSMART-Hackathon-2026) +- [Git-based IFC — Bruno Postle](https://gitaec.org/brunopostle/creative-freedom) diff --git a/docs/space_modifications.md b/docs/space_modifications.md new file mode 100644 index 0000000..a9c28a1 --- /dev/null +++ b/docs/space_modifications.md @@ -0,0 +1,119 @@ +# Space extraction and object modifications + +## Extracting a space + +The `space` command extracts an `IfcSpace` and all elements contained within it +(furniture, walls, etc.) using the ifcopenshell `location` filter. + +```bash +# by Name (room code) +uv run ifccommit.py space input.ifc out.ifc A102 + +# by LongName (human label) +uv run ifccommit.py space input.ifc out.ifc "Living Room" --by longname +``` + +Internally the query is: + +``` +IfcElement, location = "A102" +``` + +The `location` filter follows `IfcRelContainedInSpatialStructure` links — it +returns the `IfcSpace` itself plus every element directly contained in it. + +### Known limitation — aggregated spaces + +Some IFC files place spaces via `IfcRelAggregates` instead of +`IfcRelContainedInSpatialStructure`. The `location` filter only follows the +latter, so those spaces appear empty. This is a data modelling choice in the +source file, not a bug in ifcbus. + +### Multi-match with `--by longname` + +When a `LongName` matches several spaces (e.g. "Bathroom 1" → A104 and B104), +the command writes one output file per space: + +``` +dist/out_A104.ifc +dist/out_B104.ifc +``` + +The stem of the user-provided output path is reused; the space `Name` is +appended as a suffix. + +--- + +## Moving objects + +IFC element positions are stored in a 4×4 transformation matrix inside +`IfcLocalPlacement → IfcAxis2Placement3D`. The last column holds the +translation in the file's length unit (metres for duplex.ifc). + +``` +[[ r00 r01 r02 X ], + [ r10 r11 r12 Y ], + [ r20 r21 r22 Z ], + [ 0 0 0 1 ]] +``` + +### Reading and writing placement + +```python +import ifcopenshell +import ifcopenshell.util.placement +import ifcopenshell.api + +model = ifcopenshell.open("input.ifc") +el = model.by_id(17902) # entity id (not GlobalId) + +matrix = ifcopenshell.util.placement.get_local_placement(el.ObjectPlacement) + +# translate +2 m on world X axis ("right") +matrix[0, 3] += 2.0 + +ifcopenshell.api.run( + "geometry.edit_object_placement", + model, + product=el, + matrix=matrix, +) + +model.write("output.ifc") +``` + +### Finding an element by name + +Entity ids are file-specific and unstable. Prefer searching by name: + +```python +results = [ + e for e in model.by_type("IfcFurnishingElement") + if "168381" in (e.Name or "") +] +el = results[0] +``` + +Or use `ifcopenshell.util.selector.filter_elements` for richer queries. + +### Axis convention + +"Right" is ambiguous without a viewport. In duplex.ifc the world X axis +is used as the reference. Verify with the placement matrix before committing +to a direction — a 180° rotated element has its local X pointing in the +opposite direction to world X. + +### `move` command + +```bash +uv run ifccommit.py move input.ifc output.ifc --x 2 --y 0 --z 0 +``` + +Offsets are in the file's length unit (metres for duplex.ifc). Example: + +``` +uv run ifccommit.py move samples/duplex.ifc dist/duplex_moved_table.ifc 17902 --x 2 +# Element : #17902 M_Table-Coffee:0915 x 1830 x 0457mm:... +# Before : X=2.6192 Y=-15.3432 Z=0.0000 +# After : X=4.6192 Y=-15.3432 Z=0.0000 +``` diff --git a/docs/wasm.md b/docs/wasm.md new file mode 100644 index 0000000..f2bb72a --- /dev/null +++ b/docs/wasm.md @@ -0,0 +1,162 @@ +# IFC in the Browser: WASM Options + +## Goal + +Run `ifc-commit` logic (IFC extraction, modification, merge) client-side in a browser — no server, no Python install. + +--- + +## Option 1 — IfcOpenShell via Pyodide (closest drop-in) + +**Repo:** https://github.com/IfcOpenShell/wasm-wheels +**Wheel index:** https://ifcopenshell.github.io/wasm-wheels/ + +IfcOpenShell ships WASM as Pyodide wheels — Python packages compiled to `wasm32` via Emscripten. The full Python API runs inside a browser through the Pyodide runtime. + +### Setup + +```html + +``` + +```javascript +const pyodide = await loadPyodide(); +await pyodide.loadPackage("micropip"); +await pyodide.runPythonAsync(` + import micropip + await micropip.install( + "https://ifcopenshell.github.io/wasm-wheels/" + "ifcopenshell-0.8.3+34a1bc6-cp313-cp313-emscripten_4_0_9_wasm32.whl" + ) +`); +``` + +### Usage + +```python +# Identical to desktop ifccommit.py +import ifcopenshell +import ifcpatch + +model = ifcopenshell.open("input.ifc") +result = ifcpatch.execute({ + "input": "input.ifc", + "file": model, + "recipe": "ExtractElements", + "arguments": [["IfcWall", "IfcSlab"]], +}) +ifcpatch.write(result, "output.ifc") +``` + +### Available wheels (early 2026) + +| Wheel | Python | Emscripten | +|-------|--------|------------| +| `ifcopenshell-0.8.3+34a1bc6-cp313-cp313-emscripten_4_0_9_wasm32.whl` | 3.13 | 4.0.9 | +| `ifcopenshell-0.8.2+d50e806-cp312-cp312-emscripten_3_1_58_wasm32.whl` | 3.12 | 3.1.58 | + +### Pros / Cons + +| + | - | +|---|---| +| Identical API to desktop — `ifcpatch` recipes work as-is | Requires loading Pyodide (~10 MB) + Python interpreter overhead | +| `ifcpatch`, `ifcclash`, `ifctester` all available | Not on PyPI; must install from CDN wheel URL | +| Zero code changes to `ifccommit.py` | Slower than native due to WASM + Python layers | +| Models stay in browser (no server upload) | Large initial download | + +**Best fit:** A browser version that mirrors the CLI exactly — same `ExtractElements`, `PatchIfc` recipes, same Python logic. + +--- + +## Option 2 — web-ifc (C++/JS, no Python) + +**npm:** `web-ifc` +**Repo:** https://github.com/ThatOpen/engine_web-ifc (by That Open Company, formerly IFCjs) + +C++ IFC parser compiled to WASM via Emscripten, with a TypeScript API. The dominant ecosystem for browser IFC tools. + +```bash +npm install web-ifc +``` + +```javascript +import { IfcAPI } from "web-ifc"; + +const api = new IfcAPI(); +await api.Init(); +const bytes = new Uint8Array(await file.arrayBuffer()); +const modelID = api.OpenModel(bytes); +const walls = api.GetLineIDsWithType(modelID, api.IFCWALL); +api.CloseModel(modelID); +``` + +### Pros / Cons + +| + | - | +|---|---| +| Native JS/TS — no Python runtime | No `ifcpatch` recipe system | +| Smaller footprint than Pyodide | Lower-level API — more code to replicate `ifccommit` logic | +| Actively maintained, large ecosystem | No `ifcclash`, `ifctester`, `ifcdiff` equivalents | +| Runs in Web Workers (multi-threaded) | Write support is lower-level | + +**Best fit:** A rewrite of `ifc-commit` as a TypeScript/JS app, or integration into a larger BIM web app (Three.js, React, etc.). + +--- + +## Option 3 — ifc-lite (Rust/WASM + WebGPU) + +**Repo:** https://github.com/louistrue/ifc-lite +**npm scope:** `@ifc-lite/*` (e.g. `@ifc-lite/parser`, `@ifc-lite/geometry`, `@ifc-lite/query`) + +Rust-based IFC parser compiled to WASM via `wasm-bindgen`, with WebGPU rendering. Claims 5× faster geometry processing than alternatives. Supports IFC4, IFC4X3, and IFC5 (IFCX alpha). + +```bash +npm install @ifc-lite/parser @ifc-lite/query +``` + +### Pros / Cons + +| + | - | +|---|---| +| Fastest option (Rust + WebGPU) | Very new — alpha-stage API stability | +| Supports IFC5/IFCX | No `ifcpatch` equivalent | +| No Rust toolchain needed (pre-built WASM) | Requires rewriting all `ifc-commit` logic in TS | + +**Best fit:** Performance-critical geometry/visualization use cases. + +--- + +## Option 4 — IFCflow (inspiration, not a library) + +**URL:** https://ifcflow.com +**Docs repo:** https://github.com/louistrue/IfcFlow-Docs + +A browser-based visual node editor for IFC pipelines. Uses the IfcOpenShell WASM wheels (Pyodide) for on-device processing. Not a reusable library — a hosted web app — but demonstrates that the Pyodide approach works end-to-end for extract/filter/export workflows similar to `ifc-commit`. + +--- + +## Recommended Path for ifc-commit + +### Phase 1 — Pyodide prototype (zero code changes) + +1. Create `web/index.html` with Pyodide bootstrapped. +2. On file drop/upload, pass the `ArrayBuffer` into Python as bytes. +3. Call `ifccommit.py` functions directly via `pyodide.runPythonAsync()`. +4. Offer the output `.ifc` as a download via a `Blob` URL. + +This requires no changes to `ifccommit.py`. + +### Phase 2 — web-ifc rewrite (optional, for performance) + +If Pyodide startup time (~3–5 s) or bundle size is a problem, rewrite the core operations using `web-ifc` TypeScript API. This is a significant effort — `ifcpatch` recipes must be re-implemented. + +--- + +## References + +- IfcOpenShell WASM wheels: https://github.com/IfcOpenShell/wasm-wheels +- Wheel CDN: https://ifcopenshell.github.io/wasm-wheels/ +- Pyodide: https://pyodide.org +- web-ifc: https://github.com/ThatOpen/engine_web-ifc +- ifc-lite: https://github.com/louistrue/ifc-lite +- IFCflow: https://ifcflow.com diff --git a/files/nginx/ifc-commit.conf b/files/nginx/ifc-commit.conf new file mode 100644 index 0000000..cde5865 --- /dev/null +++ b/files/nginx/ifc-commit.conf @@ -0,0 +1,19 @@ +server { + listen 80; + server_name ifc-commit.gitaec.org ifcc.gitaec.org; + + location / { + proxy_pass http://127.0.0.1:8095; + proxy_http_version 1.1; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Required for streaming responses (/api/run) + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 300s; + } +} diff --git a/files/systemd/ifc-commit.service b/files/systemd/ifc-commit.service new file mode 100644 index 0000000..c8e0efa --- /dev/null +++ b/files/systemd/ifc-commit.service @@ -0,0 +1,12 @@ +[Unit] +Description=ifc-commit web app +After=network.target + +[Service] +User={USER} +WorkingDirectory={WORKDIR} +ExecStart={WORKDIR}/.venv/bin/uvicorn webapp.main:app --host 127.0.0.1 --port 8095 +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/files/yaml/copy_columns.yaml b/files/yaml/copy_columns.yaml new file mode 100644 index 0000000..ff8806c --- /dev/null +++ b/files/yaml/copy_columns.yaml @@ -0,0 +1,20 @@ +src: ifc/office_level_0.ifc + +operations: + - name: Copy columns by tags and move 4m inside + command: copy + input: ifc/office_level_0.ifc + output: ifc/office_level_columns.ifc + tags: ["2AF131EE-40CF-400C-997D-AC1F12875FA3", "05821791-6462-4D59-8F79-ADA07DC39677", "3D87EB6D-6A50-44D9-81C3-9906C59E5A81"] + x: 4 + y: 0 + z: 0 + + - name: Show diff between source and result + command: diff + input: ifc/office_level_0.ifc + target: ifc/office_level_columns.ifc + output: office_level_columns.diff + verbose: true + +dest: ifc/office_level_columns.ifc diff --git a/files/yaml/duplex.yaml b/files/yaml/duplex.yaml new file mode 100644 index 0000000..bffd783 --- /dev/null +++ b/files/yaml/duplex.yaml @@ -0,0 +1,39 @@ +src: ifc/duplex.ifc + +operations: + - name: List + command: list + input: ifc/duplex.ifc + - name: List by type + command: info + input: ifc/duplex.ifc + ifc_type: IfcSpace + - name: Extract + command: extract + input: ifc/duplex.ifc + output: ifc/duplex_extract.ifc + type: IfcSpace + id: A102 + - name: Modify + command: modify + tag: move table + input: ifc/duplex_extract.ifc + output: ifc/duplex_modified.ifc + element: "168381" + x: 2 + - name: Merge + command: merge + tag: merge table displacement + base: ifc/duplex.ifc + space: A102 + part: ifc/duplex_modified.ifc + output: ifc/duplex_merge.ifc + - name: WriteHistory + command: history + write_psets: true + - name: ReadHistory + command: history + input: ifc/duplex_merge.ifc + output: ifc/history.json + +dest: ifc/duplex_merge.ifc diff --git a/files/yaml/duplex_history.yaml b/files/yaml/duplex_history.yaml new file mode 100644 index 0000000..9496a4c --- /dev/null +++ b/files/yaml/duplex_history.yaml @@ -0,0 +1,9 @@ +src: ifc/duplex_merge.ifc + +operations: + - name: ReadHistory + command: history + input: ifc/duplex_merge.ifc + output: ifc/history.json + +dest: ifc/duplex_merge.ifc diff --git a/files/yaml/office-storey.yaml b/files/yaml/office-storey.yaml new file mode 100644 index 0000000..2a16d55 --- /dev/null +++ b/files/yaml/office-storey.yaml @@ -0,0 +1,10 @@ +src: ifc/office.ifc + +operations: + - name: Extract storey UG and EG + command: extract + input: ifc/office.ifc + output: ifc/office_level_0.ifc + query: "IfcElement, location = UG + IfcElement, location = EG + IfcSite + IfcGeographicElement" + +dest: ifc/office_level_0.ifc diff --git a/files/yaml/office-walls.yaml b/files/yaml/office-walls.yaml new file mode 100644 index 0000000..0db0a45 --- /dev/null +++ b/files/yaml/office-walls.yaml @@ -0,0 +1,16 @@ +src: ifc/office.ifc + +operations: + - name: Extract walls + command: extract + input: ifc/office.ifc + output: ifc/office_walls.ifc + types: + - IfcWall + - IfcWallStandardCase + - IfcCurtainWall + - IfcOpeningElement + - IfcDoor + - IfcWindow + +dest: ifc/office_walls.ifc diff --git a/files/yaml/office.yaml b/files/yaml/office.yaml new file mode 100644 index 0000000..c651ec7 --- /dev/null +++ b/files/yaml/office.yaml @@ -0,0 +1,12 @@ +src: ifc/office.ifc + +operations: + - name: List + command: list + input: ifc/office.ifc + - name: Remove spaces content + command: remove + input: ifc/office.ifc + type: IfcSpace + +dest: ifc/office.ifc diff --git a/ifccommit.py b/ifccommit.py new file mode 100644 index 0000000..e28c819 --- /dev/null +++ b/ifccommit.py @@ -0,0 +1,907 @@ +#!/usr/bin/env python3 +""" +ifc-commit — slice, inspect, and compose IFC files. + +Usage: + uv run ifccommit.py list + uv run ifccommit.py info + uv run ifccommit.py extract [...] + uv run ifccommit.py insert + uv run ifccommit.py replace + uv run ifccommit.py split [IfcType|preset ...] + uv run ifccommit.py space [--by name|longname] + uv run ifccommit.py move [entity_id] [--name X] [--x N] [--y N] [--z N] + uv run ifccommit.py copy [--entity-ids N ...] [--tags X ...] [--x N] [--y N] [--z N] + uv run ifccommit.py diff [-v] +""" + +import os +import re +import tempfile +import argparse +import ifcopenshell +import ifcopenshell.util.placement +import ifcopenshell.util.selector +import ifcopenshell.api +import ifcpatch + +PRESETS = { + "walls": ["IfcWall", "IfcWallStandardCase"], + "storey": ["IfcBuildingStorey"], + "furnitures": ["IfcFurnishingElement"], +} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def resolve_types(tokens): + """Expand preset names to IFC type lists; pass through raw IfcType tokens.""" + types = [] + for token in tokens: + if token in PRESETS: + types.extend(PRESETS[token]) + else: + types.append(token) + return types + + +def extract_elements(input_path, model, query): + """Run ExtractElements and return the resulting ifcopenshell.file.""" + return ifcpatch.execute( + { + "input": input_path, + "file": model, + "recipe": "ExtractElements", + "arguments": [query], + } + ) + + +def merge_models(input_path, base, parts): + """Merge a list of ifcopenshell.file objects into base and return result.""" + return ifcpatch.execute( + { + "input": input_path, + "file": base, + "recipe": "MergeProjects", + "arguments": [parts], + } + ) + + +def write_and_report(result, path): + """Write result to path and print product count.""" + ifcpatch.write(result, path) + out = ifcopenshell.open(path) + count = sum(1 for _ in out.by_type("IfcProduct")) + print(f"Written : {path} ({count} products)") + + +def extract_with_location(input_path, ifc_types, location): + """Extract each type separately with a location filter, merge, return output path.""" + tmp_files = [] + for ifc_type in ifc_types: + src = ifcopenshell.open(input_path) + result = extract_elements( + input_path, src, f'{ifc_type}, location = "{location}"' + ) + tmp = tempfile.mktemp(suffix=".ifc") + ifcpatch.write(result, tmp) + tmp_files.append(tmp) + + out_tmp = tempfile.mktemp(suffix=".ifc") + if len(tmp_files) == 1: + os.rename(tmp_files[0], out_tmp) + else: + base = ifcopenshell.open(tmp_files[0]) + extra = [ifcopenshell.open(f) for f in tmp_files[1:]] + result = merge_models(tmp_files[0], base, extra) + ifcpatch.write(result, out_tmp) + for f in tmp_files: + os.unlink(f) + + return out_tmp + + +# --------------------------------------------------------------------------- +# Commands +# --------------------------------------------------------------------------- + + +def cmd_list(args): + """List all IFC types present in the file and their element counts.""" + model = ifcopenshell.open(args.input) + types = {} + for element in model: + t = element.is_a() + types[t] = types.get(t, 0) + 1 + for t, count in sorted(types.items(), key=lambda x: -x[1]): + print(f" {count:>6} {t}") + + +def cmd_info(args): + """Print attributes of each element matching the given IFC type.""" + model = ifcopenshell.open(args.input) + elements = model.by_type(args.ifc_type) + print(f"{len(elements)} element(s) of type {args.ifc_type}\n") + for el in elements: + info = el.get_info() + print(f" #{el.id()} {el.is_a()}") + for k, v in info.items(): + if k not in ("id", "type"): + print(f" {k}: {v}") + print() + + +def cmd_extract(args): + """Extract elements of given IFC types (or presets) into a new IFC file.""" + ifc_types = resolve_types(args.ifc_types) + query = ", ".join(ifc_types) + print(f"Opening : {args.input}") + print(f"Extracting: {query}") + model = ifcopenshell.open(args.input) + result = extract_elements(args.input, model, query) + write_and_report(result, args.output) + + +def cmd_insert(args): + """Insert (merge) a part IFC file into a base IFC file.""" + print(f"Base : {args.base}") + print(f"Part : {args.part}") + base_model = ifcopenshell.open(args.base) + part_model = ifcopenshell.open(args.part) + result = merge_models(args.base, base_model, [part_model]) + write_and_report(result, args.output) + + +def cmd_replace(args): + """Remove a space and its contents from base, then merge the part file in.""" + print(f"Base : {args.base}") + print(f"Space : {args.space}") + print(f"Part : {args.part}") + + model = ifcopenshell.open(args.base) + + spaces = [s for s in model.by_type("IfcSpace") if s.Name == args.space] + if not spaces: + print(f"No IfcSpace with Name = {args.space!r}") + return + + contained = ifcopenshell.util.selector.filter_elements( + model, f'IfcElement, location = "{args.space}"' + ) + to_remove = list(contained) + [spaces[0]] + print(f"Removing : {len(to_remove)} elements (space + contents)") + for el in to_remove: + ifcopenshell.api.run("root.remove_product", model, product=el) + + tmp = tempfile.mktemp(suffix=".ifc") + model.write(tmp) + stripped = ifcopenshell.open(tmp) + part_model = ifcopenshell.open(args.part) + result = merge_models(tmp, stripped, [part_model]) + write_and_report(result, args.output) + os.unlink(tmp) + + +def cmd_split(args): + """Split an IFC file into one file per storey, optionally filtered by type.""" + print(f"Opening : {args.input}") + model = ifcopenshell.open(args.input) + storeys = model.by_type("IfcBuildingStorey") + if not storeys: + print("No IfcBuildingStorey found.") + return + + ifc_types = resolve_types(args.ifc_types) if args.ifc_types else ["IfcElement"] + stem = os.path.splitext(os.path.basename(args.input))[0] + os.makedirs(args.outdir, exist_ok=True) + + for storey in storeys: + name = storey.Name or storey.GlobalId + safe = re.sub(r"[^\w-]", "_", name) + output = os.path.join(args.outdir, f"{stem}_{safe}.ifc") + + # location filter breaks with + operator — extract each type separately + tmp = extract_with_location(args.input, ifc_types, name) + os.rename(tmp, output) + + out_model = ifcopenshell.open(output) + count = sum(1 for _ in out_model.by_type("IfcProduct")) + print(f" {name:<20} → {output} ({count} products)") + + +def cmd_space(args): + """Extract an IfcSpace and all objects contained within it.""" + print(f"Opening : {args.input}") + model = ifcopenshell.open(args.input) + + if args.by == "longname": + matches = [s for s in model.by_type("IfcSpace") if s.LongName == args.name] + if not matches: + print(f"No IfcSpace with LongName = {args.name!r}") + return + location_names = [s.Name for s in matches] + else: + location_names = [args.name] + + stem, ext = os.path.splitext(args.output) + + for loc in location_names: + output = f"{stem}_{loc}{ext}" if len(location_names) > 1 else args.output + print(f'Extracting: IfcElement, location = "{loc}"') + src = ifcopenshell.open(args.input) + result = extract_elements(args.input, src, f'IfcElement, location = "{loc}"') + ifcpatch.write(result, output) + out_model = ifcopenshell.open(output) + spaces = list(out_model.by_type("IfcSpace")) + count = sum(1 for _ in out_model.by_type("IfcProduct")) + print(f"Written : {output} ({count} products)") + for s in spaces: + print(f" {s.Name} {s.LongName or ''}") + + +def cmd_history(args): + """Write or read Pset_GitCommit on IFC elements. + + Write mode (write_psets=True): stamp Pset_GitCommit on every IfcProduct + in each operation's output IFC, using the current HEAD commit info. + + Read mode (input present): collect Pset_GitCommit from the given IFC + file and emit the result as JSON. + """ + import json + import subprocess + import yaml as _yaml + + write_psets = getattr(args, "write_psets", False) + input_path = getattr(args, "input", None) + + # ── Read mode ───────────────────────────────────────────────────────────── + if input_path: + model = ifcopenshell.open(input_path) + records = [] + for element in model.by_type("IfcProduct"): + for rel in getattr(element, "IsDefinedBy", None) or []: + if not rel.is_a("IfcRelDefinesByProperties"): + continue + pset = rel.RelatingPropertyDefinition + if pset.Name != "Pset_GitCommit": + continue + props = {p.Name: p.NominalValue.wrappedValue for p in pset.HasProperties} + records.append({ + "id": element.id(), + "name": element.Name or "", + "type": element.is_a(), + **props, + }) + output = getattr(args, "output", None) + if output: + dirpart = os.path.dirname(output) + if dirpart: + os.makedirs(dirpart, exist_ok=True) + with open(output, "w") as f: + json.dump(records, f, indent=2) + print(f"Written : {output} ({len(records)} record(s))") + else: + print(json.dumps(records, indent=2)) + return + + # ── Write mode ──────────────────────────────────────────────────────────── + if not write_psets: + print("history: nothing to do (no input and write_psets not set)") + return + + workdir = getattr(args, "workdir", ".") + yaml_rel = getattr(args, "yaml", None) + + def _git(*cmd): + return subprocess.run( + ["git"] + list(cmd), + capture_output=True, text=True, cwd=workdir, + ).stdout.strip() + + commit_hash = _git("rev-parse", "HEAD") + commit_message = _git("log", "-1", "--pretty=%s") + commit_author = _git("log", "-1", "--pretty=%an <%ae>") + commit_date = _git("log", "-1", "--pretty=%cI") + commit_branch = _git("rev-parse", "--abbrev-ref", "HEAD") + + if not commit_hash: + print("history: no git commits found — skipping Pset write") + return + + if yaml_rel: + yaml_file = yaml_rel if os.path.isabs(yaml_rel) else os.path.join(workdir, yaml_rel) + with open(yaml_file) as f: + config = _yaml.safe_load(f) + else: + config = {"operations": []} + + ifc_ops = [ + op for op in config.get("operations", []) + if (op.get("output") or "").endswith(".ifc") and op.get("command") != "history" + ] + + def _resolve(p): + return p if os.path.isabs(p) else os.path.join(workdir, p) + + def _changed_ids(before_path, after_path): + """Return file-local IDs (in after) of elements added or moved vs before. + + Uses GlobalId for matching so that entity IDs rewritten by ifcpatch + (e.g. after ExtractElements) don't produce false positives. + """ + src = ifcopenshell.open(before_path) + tgt = ifcopenshell.open(after_path) + src_map = {e.GlobalId: e for e in src.by_type("IfcProduct") if getattr(e, "GlobalId", None)} + tgt_map = {e.GlobalId: e for e in tgt.by_type("IfcProduct") if getattr(e, "GlobalId", None)} + changed = set() + for gid, tgt_el in tgt_map.items(): + if gid not in src_map: + changed.add(tgt_el.id()) # truly new element + continue + try: + sp = ifcopenshell.util.placement.get_local_placement(src_map[gid].ObjectPlacement) + tp = ifcopenshell.util.placement.get_local_placement(tgt_el.ObjectPlacement) + if any(abs(sp[i, 3] - tp[i, 3]) > 0.001 for i in range(3)): + changed.add(tgt_el.id()) + except Exception: + pass + return changed + + total = 0 + for op in ifc_ops: + out_abs = _resolve(op["output"]) + if not os.path.exists(out_abs): + print(f" skip (not found): {op['output']}") + continue + + # Determine comparison baseline: input or base + before_rel = op.get("input") or op.get("base") + before_abs = _resolve(before_rel) if before_rel else None + + if before_abs and os.path.exists(before_abs): + changed = _changed_ids(before_abs, out_abs) + else: + # No baseline — skip (nothing to compare against) + print(f" skip (no baseline): {op['output']}") + continue + + if not changed: + print(f" no changes: {op['output']}") + continue + + model = ifcopenshell.open(out_abs) + # Clear any existing Pset_GitCommit left from prior runs + for rel in list(model.by_type("IfcRelDefinesByProperties")): + pset = rel.RelatingPropertyDefinition + if getattr(pset, "Name", None) == "Pset_GitCommit": + for product in rel.RelatedObjects: + ifcopenshell.api.run("pset.remove_pset", model, product=product, pset=pset) + count = 0 + for eid in changed: + element = model.by_id(eid) + if element is None: + continue + pset = ifcopenshell.api.run("pset.add_pset", model, product=element, name="Pset_GitCommit") + ifcopenshell.api.run("pset.edit_pset", model, pset=pset, properties={ + "CommitHash": commit_hash, + "CommitMessage": commit_message, + "CommitAuthor": commit_author, + "CommitDate": commit_date, + "CommitBranch": commit_branch, + "OperationName": op["name"], + }) + count += 1 + model.write(out_abs) + total += count + print(f" Pset_GitCommit: {op['output']} ({count} changed product(s))") + + print(f"Written : Pset_GitCommit on {total} changed product(s) across {len(ifc_ops)} file(s)") + + +def cmd_move(args): + """Translate an element by (x, y, z) metres and write to a new file.""" + print(f"Opening : {args.input}") + model = ifcopenshell.open(args.input) + + if args.name: + matches = [ + e for e in model.by_type("IfcProduct") if args.name in (e.Name or "") + ] + if not matches: + print(f"No IfcProduct with name containing {args.name!r}") + return + if len(matches) > 1: + print(f"Multiple matches for {args.name!r}:") + for m in matches: + print(f" #{m.id()} {m.Name}") + return + el = matches[0] + elif args.entity_id is not None: + el = model.by_id(args.entity_id) + else: + print("No element specified (name or entity_id required)") + return + if el is None: + print(f"No element found with id={args.entity_id}") + return + print(f"Element : #{el.id()} {el.Name}") + + matrix = ifcopenshell.util.placement.get_local_placement(el.ObjectPlacement) + print( + f"Before : X={matrix[0, 3]:.4f} Y={matrix[1, 3]:.4f} Z={matrix[2, 3]:.4f}" + ) + + matrix[0, 3] += args.x + matrix[1, 3] += args.y + matrix[2, 3] += args.z + ifcopenshell.api.run( + "geometry.edit_object_placement", model, product=el, matrix=matrix + ) + + after = ifcopenshell.util.placement.get_local_placement(el.ObjectPlacement) + print(f"After : X={after[0, 3]:.4f} Y={after[1, 3]:.4f} Z={after[2, 3]:.4f}") + + model.write(args.output) + out_model = ifcopenshell.open(args.output) + count = sum(1 for _ in out_model.by_type("IfcProduct")) + print(f"Written : {args.output} ({count} products)") + + +def cmd_copy(args): + """Copy elements by entity_ids or tags and translate copies by (x, y, z) metres.""" + print(f"Opening : {args.input}") + model = ifcopenshell.open(args.input) + + elements = [] + if args.tags: + for tag in args.tags: + matches = [] + for e in model.by_type("IfcProduct"): + try: + if e.Tag == tag: + matches.append(e) + except AttributeError: + pass + if not matches: + print(f"Warning: No element found with tag={tag}") + continue + elements.extend(matches) + elif args.entity_ids: + for eid in args.entity_ids: + el = model.by_id(eid) + if el is None: + print(f"Warning: No element found with id={eid}") + continue + elements.append(el) + else: + print("No elements specified (tags or entity_ids required)") + return + + if not elements: + print("No elements found") + return + + copies = [] + for el in elements: + print(f"Copying : #{el.id()} {el.Name or el.is_a()}") + new_el = ifcopenshell.api.run("root.copy_class", model, product=el) + copies.append(new_el) + + matrix = ifcopenshell.util.placement.get_local_placement(new_el.ObjectPlacement) + matrix[0, 3] += args.x + matrix[1, 3] += args.y + matrix[2, 3] += args.z + ifcopenshell.api.run( + "geometry.edit_object_placement", model, product=new_el, matrix=matrix + ) + + after = ifcopenshell.util.placement.get_local_placement(new_el.ObjectPlacement) + print( + f" After : X={after[0, 3]:.4f} Y={after[1, 3]:.4f} Z={after[2, 3]:.4f}" + ) + + model.write(args.output) + out_model = ifcopenshell.open(args.output) + count = sum(1 for _ in out_model.by_type("IfcProduct")) + print(f"Written : {args.output} ({count} products)") + + +def cmd_diff(args): + """Show differences between two IFC files and write to output file.""" + print(f"Source : {args.source}") + print(f"Target : {args.target}") + + src_model = ifcopenshell.open(args.source) + tgt_model = ifcopenshell.open(args.target) + + src_elements = {e.id(): e for e in src_model.by_type("IfcProduct")} + tgt_elements = {e.id(): e for e in tgt_model.by_type("IfcProduct")} + + src_ids = set(src_elements.keys()) + tgt_ids = set(tgt_elements.keys()) + + added = tgt_ids - src_ids + removed = src_ids - tgt_ids + common = src_ids & tgt_ids + + lines = [] + lines.append(f"Source : {args.source}") + lines.append(f"Target : {args.target}") + lines.append("") + lines.append("Summary:") + lines.append(f" Added : {len(added)} element(s)") + lines.append(f" Removed : {len(removed)} element(s)") + lines.append(f" Modified: {len(common)} element(s)") + + if args.verbose and (added or removed): + lines.append("") + lines.append("Added elements:") + for eid in sorted(added): + el = tgt_elements[eid] + lines.append(f" +{eid} {el.is_a()} {el.Name or ''}") + + lines.append("") + lines.append("Removed elements:") + for eid in sorted(removed): + el = src_elements[eid] + lines.append(f" -{eid} {el.is_a()} {el.Name or ''}") + + if args.verbose: + lines.append("") + lines.append("Modified elements:") + for eid in sorted(common): + src_el = src_elements[eid] + tgt_el = tgt_elements[eid] + + src_placement = None + tgt_placement = None + + try: + src_placement = ifcopenshell.util.placement.get_local_placement( + src_el.ObjectPlacement + ) + except: + pass + + try: + tgt_placement = ifcopenshell.util.placement.get_local_placement( + tgt_el.ObjectPlacement + ) + except: + pass + + if src_placement is not None and tgt_placement is not None: + src_coords = ( + src_placement[0, 3], + src_placement[1, 3], + src_placement[2, 3], + ) + tgt_coords = ( + tgt_placement[0, 3], + tgt_placement[1, 3], + tgt_placement[2, 3], + ) + + if ( + abs(src_coords[0] - tgt_coords[0]) > 0.001 + or abs(src_coords[1] - tgt_coords[1]) > 0.001 + or abs(src_coords[2] - tgt_coords[2]) > 0.001 + ): + lines.append(f" #{eid} {tgt_el.is_a()} {tgt_el.Name or ''}") + lines.append( + f" X: {src_coords[0]:.4f} -> {tgt_coords[0]:.4f} (delta: {tgt_coords[0] - src_coords[0]:+.4f})" + ) + lines.append( + f" Y: {src_coords[1]:.4f} -> {tgt_coords[1]:.4f} (delta: {tgt_coords[1] - src_coords[1]:+.4f})" + ) + lines.append( + f" Z: {src_coords[2]:.4f} -> {tgt_coords[2]:.4f} (delta: {tgt_coords[2] - src_coords[2]:+.4f})" + ) + + output = "\n".join(lines) + print(output) + + if args.output: + with open(args.output, "w") as f: + f.write(output + "\n") + print(f"\nWritten : {args.output}") + + +def cmd_remove(args): + """Remove all elements contained within each instance of the given IFC type.""" + print(f"Opening : {args.input}") + model = ifcopenshell.open(args.input) + + containers = model.by_type(args.ifc_type) + if not containers: + print(f"No {args.ifc_type} found in {args.input}") + return + + removed_ids = set() + total = 0 + for container in containers: + name = container.Name or container.GlobalId + contained = [ + el + for el in ifcopenshell.util.selector.filter_elements( + model, f'IfcElement, location = "{name}"' + ) + if el.id() not in removed_ids and not el.is_a("IfcOpeningElement") + ] + print(f" {args.ifc_type} {name!r}: {len(contained)} element(s) removed") + for el in contained: + removed_ids.add(el.id()) + ifcopenshell.api.run("root.remove_product", model, product=el) + total += len(contained) + + stem, ext = os.path.splitext(args.input) + type_safe = re.sub(r"[^\w]", "", args.ifc_type) + output = args.output or f"{stem}_{type_safe}_removed{ext}" + + model.write(output) + out_model = ifcopenshell.open(output) + count = sum(1 for _ in out_model.by_type("IfcProduct")) + print(f"Removed : {total} element(s) total") + print(f"Written : {output} ({count} products)") + + +def cmd_run(args): + """Fetch yaml pipelines from the demo repo, let user pick one, and execute it.""" + import base64 + import json + import shutil + import subprocess + from urllib.request import urlopen + from urllib.error import URLError + + DEMO_REPO = "rvba/ifc-commit" + FORGE_API = "https://gitaec.org/api/v1" + + # 1. List yaml files in yaml/ on the demo repo + url = f"{FORGE_API}/repos/{DEMO_REPO}/contents/yaml" + try: + with urlopen(url) as resp: + entries = json.loads(resp.read()) + except URLError as exc: + print(f"Cannot reach demo repo: {exc}") + return + + yaml_files = [ + e + for e in entries + if e.get("type") == "file" and e["name"].endswith((".yaml", ".yml")) + ] + if not yaml_files: + print("No yaml files found in yaml/ on demo repo.") + return + + # 2. Let user pick + print("Pipelines available on demo repo:") + for i, f in enumerate(yaml_files, 1): + print(f" {i}. {f['name']}") + + try: + choice = input("\nSelect (number): ").strip() + idx = int(choice) - 1 + if not (0 <= idx < len(yaml_files)): + raise ValueError + selected = yaml_files[idx] + except (ValueError, EOFError): + print("Invalid selection.") + return + + yaml_path = f"yaml/{selected['name']}" + print(f"Selected : {yaml_path}") + + # 3. Fetch yaml content via API + url = f"{FORGE_API}/repos/{DEMO_REPO}/contents/{yaml_path}" + try: + with urlopen(url) as resp: + data = json.loads(resp.read()) + content = base64.b64decode(data["content"]).decode() + except (URLError, KeyError) as exc: + print(f"Cannot fetch yaml: {exc}") + return + + # 4. Shallow-clone demo repo into a temp dir + clone_url = f"https://gitaec.org/{DEMO_REPO}.git" + workdir = tempfile.mkdtemp(prefix="ifccommit_run_") + print(f"Cloning demo repo...") + try: + subprocess.run( + ["git", "clone", "--depth=1", clone_url, workdir], + check=True, + capture_output=True, + text=True, + ) + except subprocess.CalledProcessError as exc: + print(f"Clone failed:\n{exc.stdout}") + shutil.rmtree(workdir, ignore_errors=True) + return + + # 5. Parse yaml and run pipeline + import sys + + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from webapp.pipeline import parse_yaml, run_pipeline + + try: + config = parse_yaml(content) + except Exception as exc: + print(f"Invalid yaml: {exc}") + shutil.rmtree(workdir, ignore_errors=True) + return + + print(f"Running : {selected['name']} ({len(config.operations)} operation(s))\n") + run_pipeline(config, workdir, print, yaml_path=yaml_path) + shutil.rmtree(workdir, ignore_errors=True) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + prog="ifccommit", + description="ifc-commit — slice, inspect, and compose IFC files", + ) + sub = parser.add_subparsers(dest="command", required=True) + + p_list = sub.add_parser("list", help="List IFC types in file") + p_list.add_argument("input", help="Input IFC file") + + p_info = sub.add_parser("info", help="Show element attributes for a type") + p_info.add_argument("input", help="Input IFC file") + p_info.add_argument("ifc_type", metavar="IfcType", help="e.g. IfcWall") + + p_ext = sub.add_parser("extract", help="Extract elements to a new IFC file") + p_ext.add_argument("input", help="Input IFC file") + p_ext.add_argument("output", help="Output IFC file") + p_ext.add_argument( + "ifc_types", + metavar="IfcType|preset", + nargs="+", + help=f"IFC types or presets: {', '.join(PRESETS)} (e.g. walls, IfcSlab)", + ) + + p_ins = sub.add_parser("insert", help="Insert a part IFC file into a base IFC file") + p_ins.add_argument("base", help="Base IFC file") + p_ins.add_argument("part", help="Part IFC file to insert") + p_ins.add_argument("output", help="Output IFC file") + + p_rep = sub.add_parser( + "replace", help="Remove a space from base and merge a part in its place" + ) + p_rep.add_argument("base", help="Base IFC file") + p_rep.add_argument("space", help="Space Name to remove (e.g. A102)") + p_rep.add_argument("part", help="Part IFC file to merge in") + p_rep.add_argument("output", help="Output IFC file") + + p_spl = sub.add_parser("split", help="Split into one IFC file per storey") + p_spl.add_argument("input", help="Input IFC file") + p_spl.add_argument("outdir", help="Output directory") + p_spl.add_argument( + "ifc_types", + metavar="IfcType|preset", + nargs="*", + help=f"Optional type filter: presets {', '.join(PRESETS)} or raw IFC types", + ) + + p_spc = sub.add_parser("space", help="Extract a space and all its contents") + p_spc.add_argument("input", help="Input IFC file") + p_spc.add_argument("output", help="Output IFC file") + p_spc.add_argument("name", help="Space name to match") + p_spc.add_argument( + "--by", + choices=["name", "longname"], + default="name", + help="Match against Name (default) or LongName", + ) + + p_his = sub.add_parser( + "history", help="Write or read Pset_GitCommit on IFC elements" + ) + p_his.add_argument( + "--input", help="IFC file to read Pset_GitCommit from (read mode)" + ) + p_his.add_argument( + "--output", help="Output JSON file (read mode) or unused in write mode" + ) + p_his.add_argument( + "--write-psets", dest="write_psets", action="store_true", + help="Stamp Pset_GitCommit on all output IFC elements using HEAD (write mode)", + ) + p_his.add_argument( + "--workdir", default=".", help="Git repository root (default: cwd)" + ) + p_his.add_argument( + "--yaml", + default="yaml/duplex.yaml", + help="Repo-relative path to the pipeline yaml (write mode)", + ) + + p_rem = sub.add_parser( + "remove", + help="Remove all elements contained within each instance of an IFC type", + ) + p_rem.add_argument("input", help="Input IFC file") + p_rem.add_argument( + "ifc_type", metavar="IfcType", help="Container type (e.g. IfcSpace)" + ) + p_rem.add_argument( + "--output", help="Output IFC file (default: __removed.ifc)" + ) + + sub.add_parser("run", help="Browse and run a pipeline yaml from the demo repo") + + p_mov = sub.add_parser("move", help="Translate an element by (x, y, z) metres") + p_mov.add_argument("input", help="Input IFC file") + p_mov.add_argument("output", help="Output IFC file") + p_mov.add_argument("--name", help="Find element by name substring") + p_mov.add_argument( + "entity_id", type=int, nargs="?", help="IFC entity id (e.g. 17902)" + ) + p_mov.add_argument("--x", type=float, default=0.0, help="X offset in metres") + p_mov.add_argument("--y", type=float, default=0.0, help="Y offset in metres") + p_mov.add_argument("--z", type=float, default=0.0, help="Z offset in metres") + + p_cpy = sub.add_parser( + "copy", + help="Copy elements by ids or tags and translate copies by (x, y, z) metres", + ) + p_cpy.add_argument("input", help="Input IFC file") + p_cpy.add_argument("output", help="Output IFC file") + p_cpy.add_argument( + "--entity-ids", + type=int, + nargs="+", + help="IFC entity ids to copy (e.g. 17902 17903)", + ) + p_cpy.add_argument( + "--tags", + type=str, + nargs="+", + help="IFC element tags to copy (e.g. ABC123 DEF456)", + ) + p_cpy.add_argument("--x", type=float, default=0.0, help="X offset in metres") + p_cpy.add_argument("--y", type=float, default=0.0, help="Y offset in metres") + p_cpy.add_argument("--z", type=float, default=0.0, help="Z offset in metres") + + p_diff = sub.add_parser("diff", help="Show differences between two IFC files") + p_diff.add_argument("source", help="Source IFC file") + p_diff.add_argument("target", help="Target IFC file to compare against") + p_diff.add_argument( + "--verbose", "-v", action="store_true", help="Show detailed changes" + ) + p_diff.add_argument("--output", "-o", help="Output file to write diff results") + + args = parser.parse_args() + + dispatch = { + "list": cmd_list, + "info": cmd_info, + "extract": cmd_extract, + "insert": cmd_insert, + "replace": cmd_replace, + "split": cmd_split, + "space": cmd_space, + "move": cmd_move, + "copy": cmd_copy, + "diff": cmd_diff, + "history": cmd_history, + "remove": cmd_remove, + "run": cmd_run, + } + dispatch[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1268ef7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "ifccommit" +version = "0.1.0" +description = "ifc-commit — extract, merge, and commit IFC parts" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "ifcopenshell>=0.8.4.post1", + "ifcpatch>=0.8.4", + "fastapi>=0.111", + "uvicorn[standard]>=0.29", + "httpx>=0.27", + "pyyaml>=6.0", + "pydantic>=2.0", + "markdown-it-py>=4.0.0", +] + +[project.scripts] +ifccommit = "ifccommit:main" +ifccommit-web = "webapp.main:serve" + +[dependency-groups] +dev = [ + "pytest>=9.0.2", +] + +[tool.pytest.ini_options] +pythonpath = ["."] diff --git a/scripts/clone_demo_repo.sh b/scripts/clone_demo_repo.sh new file mode 100644 index 0000000..936a881 --- /dev/null +++ b/scripts/clone_demo_repo.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [[ ! -d demo ]]; then + mkdir -p demo +fi + +if [[ ! -d demo/ifc-commit-demo ]]; then + cd demo && git clone https://gitaec.org/rvba/ifc-commit-demo +else + echo "removing ifc-commit-demo" + rm -fr demo/ifc-commit-demo + cd demo && git clone https://gitaec.org/rvba/ifc-commit-demo +fi diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..d31e655 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# release.sh — copy source repo to ifc-commit-release, filtering dev-only files. +# Usage: bash scripts/release.sh [/path/to/ifc-commit-release] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SRC="$(cd "$SCRIPT_DIR/.." && pwd)" +DEST="${1:-$SRC/../ifc-commit-release}" + +if [[ ! -d "$DEST" ]]; then + echo "Destination not found: $DEST" + echo "Cloning ifc-commit-release via nx..." + (cd "$(dirname "$DEST")" && nx git clone forge ifc-commit-release) + if [[ ! -d "$DEST" ]]; then + echo "Error: clone failed, $DEST still not found." + exit 1 + fi +fi + +echo "Source : $SRC" +echo "Dest : $DEST" +echo + +# Sync files, excluding dev-only and build artefacts +rsync -av --delete \ + --exclude='.git' \ + --exclude='CLAUDE.md' \ + --exclude='AGENT.md' \ + --exclude='AGENT_RELEASE.md' \ + --exclude='__pycache__/' \ + --exclude='*.pyc' \ + --exclude='*.pyo' \ + --exclude='.venv/' \ + --exclude='dist/' \ + --exclude='demo/' \ + --exclude='ifc/' \ + --exclude='*.ifc' \ + --exclude='index.html' \ + --exclude='research.html' \ + "$SRC/" "$DEST/" + +# Copy AGENT_RELEASE.md as AGENT.md in the release repo +echo +echo "Installing AGENT_RELEASE.md → AGENT.md" +cp "$SRC/AGENT_RELEASE.md" "$DEST/AGENT.md" + +echo +echo "Done. Review changes in $DEST before committing and pushing." diff --git a/scripts/reset_demo_repo.sh b/scripts/reset_demo_repo.sh new file mode 100755 index 0000000..60633a7 --- /dev/null +++ b/scripts/reset_demo_repo.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# reset_demo_repo.sh +# +# Clones the gitaec.org demo repo into a temp directory, strips unwanted IFC +# files, resets OPERATIONS.md, squashes all history into a single "init" commit, +# and force-pushes to origin. +# +# Files kept in ifc/: duplex.ifc office.ifc +# History: replaced with a single initial commit + +set -euo pipefail + +DEMO_REPO="https://gitaec.org/rvba/ifc-commit-demo.git" + +echo "⚠️ WARNING: This will erase ALL git history of the demo repo." +echo " Repo : $DEMO_REPO" +echo " Kept : ifc/duplex.ifc ifc/office.ifc yaml/" +echo " Removed: OPERATIONS.md (if present)" +echo "" +read -r -p "Type 'yes' to proceed: " CONFIRM +if [ "$CONFIRM" != "yes" ]; then + echo "Aborted." + exit 1 +fi +echo "" +WORK_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +echo "==> Cloning demo repo into $WORK_DIR" +git clone "$DEMO_REPO" "$WORK_DIR/demo" +cd "$WORK_DIR/demo" + +echo "==> Removing unwanted IFC files from ifc/" +if [ -d ifc ]; then + find ifc -maxdepth 1 -name "*.ifc" \ + ! -name "duplex.ifc" \ + ! -name "office.ifc" \ + -delete + echo " Kept: $(ls ifc/*.ifc 2>/dev/null | xargs -n1 basename | tr '\n' ' ')" +else + echo " WARNING: ifc/ directory not found — skipping IFC cleanup" +fi + +echo "==> Keeping yaml/ directory intact" +if [ ! -d yaml ]; then + echo " WARNING: yaml/ directory not found" +fi + +echo "==> Removing OPERATIONS.md and ifc/history.json" +rm -f OPERATIONS.md ifc/history.json + +echo "==> Switching to orphan branch to drop all history" +git checkout --orphan fresh-start + +echo "==> Staging all remaining files" +git add -A + +echo "==> Creating single initial commit" +git commit -m "init: fresh demo repo" + +echo "==> Force-pushing to origin as main" +git push --force origin fresh-start:main + +echo "" +echo "Done. Demo repo history has been reset." +echo "Verify at: https://gitaec.org/rvba/ifc-commit-demo" diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..9b8d346 --- /dev/null +++ b/uv.lock @@ -0,0 +1,932 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +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, 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, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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, 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, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[[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 = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "ifccommit" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "ifcopenshell" }, + { name = "ifcpatch" }, + { name = "markdown-it-py" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.111" }, + { name = "httpx", specifier = ">=0.27" }, + { name = "ifcopenshell", specifier = ">=0.8.4.post1" }, + { name = "ifcpatch", specifier = ">=0.8.4" }, + { name = "markdown-it-py", specifier = ">=4.0.0" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.29" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.2" }] + +[[package]] +name = "ifcopenshell" +version = "0.8.4.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "lark" }, + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "shapely" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/1d/50561eed4001f65b55568254bde4212ea543dad5d82b4ea19ce96d2beb1c/ifcopenshell-0.8.4.post1-py310-none-macosx_10_15_x86_64.whl", hash = "sha256:0d8749bc9741e06280b84a05bb23e3c6b84b60bf8268e9ad022895097a36b6ce", size = 44707468, upload-time = "2025-12-19T12:55:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/64/a8/3b6ca588d3756832e69e659cea313d979cedc7bc5435d548777d5e31ec6a/ifcopenshell-0.8.4.post1-py310-none-macosx_11_0_arm64.whl", hash = "sha256:c404474f421d64746563a4a8607b116cb31d0edef058a7e51c6383e221654486", size = 42129009, upload-time = "2025-12-19T12:55:17.623Z" }, + { url = "https://files.pythonhosted.org/packages/e8/84/6002a38863b7a411bf83e2cd80b433cfd3b4b0b870a02e5323fbf2ec8d21/ifcopenshell-0.8.4.post1-py310-none-manylinux_2_31_aarch64.whl", hash = "sha256:51643c1455da6e8bed68589c5c8a354b81bb37d72ac4fb86153f0498cde0b809", size = 39711784, upload-time = "2025-12-19T12:55:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/395c997e7f633df79d8049dd530a3566b034358aa759118fb79b463e1ef3/ifcopenshell-0.8.4.post1-py310-none-manylinux_2_31_x86_64.whl", hash = "sha256:3ce52bbf24aeff667667bf90cbb80387e497806186cfde6772f7d0c4530b6333", size = 42599969, upload-time = "2025-12-19T12:55:35.818Z" }, + { url = "https://files.pythonhosted.org/packages/03/eb/da54d7f5b851a004659e90074c6ca6d8792b269b86e29141206d591f0ab9/ifcopenshell-0.8.4.post1-py310-none-win_amd64.whl", hash = "sha256:60c7008391e27bacbadf4820945f95d177772bc88f2ce2fd58ae7c921b38b6f2", size = 24421529, upload-time = "2025-12-19T12:54:06.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/38f863c3233ab814312f221f700c4015da1ed42f42b533390e6f02a20899/ifcopenshell-0.8.4.post1-py311-none-macosx_10_15_x86_64.whl", hash = "sha256:908bf4f1f79205382762f813e6f9ffac33ca947636cdae6d489cdc2ed2b31096", size = 44707468, upload-time = "2025-12-19T12:55:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/c5138189e58c56a4954f3cc7c696eaa1daa7f08bbfa3569fad87d4140e64/ifcopenshell-0.8.4.post1-py311-none-macosx_11_0_arm64.whl", hash = "sha256:4d9ee560ea397aaacd692a985c4dec24fd35cf4b8e41dd6334efc4e6beb96261", size = 42129000, upload-time = "2025-12-19T12:54:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/32b1c52c00c92137cd06ec42bac4ae8ba6a3fd091240c437b6c34c6fbab8/ifcopenshell-0.8.4.post1-py311-none-manylinux_2_31_aarch64.whl", hash = "sha256:35f876650d20d4efb038b1b0eca8f0acef35b31936954ac4ad6b40186b45e381", size = 39712524, upload-time = "2025-12-19T12:54:20.227Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5d/38ed682bca572525d702e3c03a17cd82a8b4585d9ef613921e7924be2c85/ifcopenshell-0.8.4.post1-py311-none-manylinux_2_31_x86_64.whl", hash = "sha256:07d12a4ccb95219b8a53e046c079826c59585b27861fdd308f7f223b80a83253", size = 42600723, upload-time = "2025-12-19T12:54:24.372Z" }, + { url = "https://files.pythonhosted.org/packages/a8/41/2ad10bf083d868a4e0dd7e2787060c45e9ef91d62f2b6229a9552f921035/ifcopenshell-0.8.4.post1-py311-none-win_amd64.whl", hash = "sha256:5e977b4e7ee9e77bdc1b3f529e8a1c3c0df13ae0fb72400008595935ba09a199", size = 24421279, upload-time = "2025-12-19T12:55:14.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/90/42a3cdecaa9ceeb91911548f5742855fd2ea7407b0c5a50702a600b12660/ifcopenshell-0.8.4.post1-py312-none-macosx_10_15_x86_64.whl", hash = "sha256:622b9c7da5d54071af976206d35fb82501d00ac96dec420d3abdc16483ef3118", size = 44723876, upload-time = "2025-12-19T12:54:20.052Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/295f8e92005f5d81c5634b52fbe340cd3576805725572e9cc80cdf5629bf/ifcopenshell-0.8.4.post1-py312-none-macosx_11_0_arm64.whl", hash = "sha256:24580fb942e4c3530ea8bb404d679962b79f892ffb5fa8cb72f5cdd6a1ac451e", size = 42131881, upload-time = "2025-12-19T12:55:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/33/29/3331d2b684a141b5ea38152b0f135db91b98ee74209a6228a508a532c1e5/ifcopenshell-0.8.4.post1-py312-none-manylinux_2_31_aarch64.whl", hash = "sha256:904ff0f1e44c453163b29f8d170b6e4c0c3413128e63add09642e26a2ebf4388", size = 39715443, upload-time = "2025-12-19T12:54:17.869Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e1/a51359f52ab36e0acc1e0c234421b6c34af7e2384d141361d6cc9b84b49b/ifcopenshell-0.8.4.post1-py312-none-manylinux_2_31_x86_64.whl", hash = "sha256:0945e3107989d94206aac00e14da7ddf57fcf932ad5fd52bf2686f6d8b5b199b", size = 42603685, upload-time = "2025-12-19T12:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/39/61/e77cae8a964b249497a3a74b953a02f677b7790ec05835b6e8a72de64bd8/ifcopenshell-0.8.4.post1-py312-none-win_amd64.whl", hash = "sha256:c19860aa2e764b3bc51088369192651122e013b58880cc2b5bfeb8c5a44034eb", size = 24423977, upload-time = "2025-12-19T12:54:10.758Z" }, + { url = "https://files.pythonhosted.org/packages/9f/63/4824b346c0ab15a7082fb6513ca9cf5dd6e8d2bdc8f29f60b0b824e0f965/ifcopenshell-0.8.4.post1-py313-none-macosx_10_15_x86_64.whl", hash = "sha256:1e5ec05c77156b4ade1ef16b70b5065888b96552645032f10795edf5f3bafb22", size = 44723880, upload-time = "2025-12-19T12:55:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/cb/fa/81085742ac3e58a0a2b85da29d3b4d3ca64cc3766467e65cc9c9053cb833/ifcopenshell-0.8.4.post1-py313-none-macosx_11_0_arm64.whl", hash = "sha256:ad7cf4dacbf6a855b1ec9ba82bbc9d737d74aff2b0311febb1037401201684c2", size = 42131883, upload-time = "2025-12-19T12:54:15.529Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6f/2f8c4f64796c993f7014c77f8d38ddc0e073cac4ad3efab64231cdfbd655/ifcopenshell-0.8.4.post1-py313-none-manylinux_2_31_aarch64.whl", hash = "sha256:ae1c75b42768db0c40d3d7de0a17f1836488591908f45c9dd3fa5b993ee2b61b", size = 39713877, upload-time = "2025-12-19T12:54:21.245Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b6/57017c627ad81c4f1c6443451b4d197f010a8258fbe44fd76b8a877f1e6b/ifcopenshell-0.8.4.post1-py313-none-manylinux_2_31_x86_64.whl", hash = "sha256:972103990d182ad6224870f869536d9ea10bf3fad36a8ee3d6a8ab208dccac78", size = 42603371, upload-time = "2025-12-19T12:54:20.45Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d0/9bd6f9a84c946b7e48728981e6301fc5d4f929a13aa2a4b9364014799318/ifcopenshell-0.8.4.post1-py313-none-win_amd64.whl", hash = "sha256:bbcfcbc376f8c35d9ad4c8e8ffb398477aad32c08d301e45659bb72391f93abf", size = 24421513, upload-time = "2025-12-19T12:54:04.515Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f2/6dfc14554a744c9022eb7c3a5f3e10d5d402c88773df078141316f60dc67/ifcopenshell-0.8.4.post1-py314-none-macosx_10_15_x86_64.whl", hash = "sha256:3f371edc2ec9c20f13132f58fd80ebff7deeecf3c2d0befb8912fff8a7cd0ab2", size = 44724144, upload-time = "2025-12-19T12:54:19.792Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/592141bbde639c7c23ac47a6b6a95979ebcb87d1b6a5ccf14d09aef4c9de/ifcopenshell-0.8.4.post1-py314-none-macosx_11_0_arm64.whl", hash = "sha256:6a97b49e42b1764fed16b21def59bfe24189e0fa99f663d919f0a10df6be3a2d", size = 42132885, upload-time = "2025-12-19T12:55:06.734Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/7aea45cf1b583a75a26b8aeada257c2107bdd368d0ae6fc0495ab081eeae/ifcopenshell-0.8.4.post1-py314-none-manylinux_2_31_aarch64.whl", hash = "sha256:9a4a24478c98643f138dc309e8abf3bb07c2c869176f6210763f1abce21cea93", size = 39714877, upload-time = "2025-12-19T12:54:27.928Z" }, + { url = "https://files.pythonhosted.org/packages/87/72/ddcdd0f311aeaceb29e2a2fbcffa61ab59e5beab61f93a5a327e769e4bea/ifcopenshell-0.8.4.post1-py314-none-manylinux_2_31_x86_64.whl", hash = "sha256:9831ed68449864069b5fe34ae1c5ab7cd1a3c5cbcc6d4209aff47a4081d64ae2", size = 42604101, upload-time = "2025-12-19T12:54:22.479Z" }, + { url = "https://files.pythonhosted.org/packages/d2/99/010cd6f9c4212ab5e50e271b9624d461bf473a15ba15d99c340442ac4688/ifcopenshell-0.8.4.post1-py314-none-win_amd64.whl", hash = "sha256:901981a806b3614d52c4cb66d34feafafcbdea7351bb4271c1385c2321d6f76f", size = 24424281, upload-time = "2025-12-19T12:54:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/39/31/8563acef573677582335463b8658dce46d964a4463fcf3ccb7a8d8584219/ifcopenshell-0.8.4.post1-py39-none-macosx_10_15_x86_64.whl", hash = "sha256:503661af146648c393f819156570cd56948e4c6da3aa3a700c4d8119e96dfef6", size = 44707483, upload-time = "2025-12-19T12:54:20.648Z" }, + { url = "https://files.pythonhosted.org/packages/a4/74/40614bf534b578184baf4298e4a2125ffa0df3fadf17f8e5c245303a8c4d/ifcopenshell-0.8.4.post1-py39-none-macosx_11_0_arm64.whl", hash = "sha256:ff7ea50b64200ff87bdaaef04d7cca1bf8e22c25b81cb525ff8d440f8ad27cd4", size = 42129009, upload-time = "2025-12-19T12:54:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/f4a23b7892c9b041a2a5a50438479eda849ade7deeec18b285563a1937d0/ifcopenshell-0.8.4.post1-py39-none-manylinux_2_31_aarch64.whl", hash = "sha256:3b722d1f875e47e0253310451458b05aff5eb0277d83dcab7009250fdfcfa5d4", size = 39709209, upload-time = "2025-12-19T12:54:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d9/a5c9b36a0da73c895cd437855d7844c7f311b4fbd23bcad413eb61326b07/ifcopenshell-0.8.4.post1-py39-none-manylinux_2_31_x86_64.whl", hash = "sha256:ea96756112ef047ce7eb3c1380b6dfdbe724b5fc724a9b02578d547fb19be07d", size = 42600043, upload-time = "2025-12-19T12:55:30.505Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7c/e76d81a7b6e2a39928e3b6a2329be9637de3421d883047a178c927e73407/ifcopenshell-0.8.4.post1-py39-none-win_amd64.whl", hash = "sha256:c56d1e6733e780596cc202309b34a8044ceacfc007bb12728b4598eca243f821", size = 24421469, upload-time = "2025-12-19T12:54:07.483Z" }, +] + +[[package]] +name = "ifcpatch" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ifcopenshell" }, + { name = "numpy" }, + { name = "toposort" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/30/bc40c86efd55e622c66e02753ec91e1cbb86f8f576499686a285cad6db0d/ifcpatch-0.8.4-py3-none-any.whl", hash = "sha256:0446f7626925e4d9d17a4096a3db1ae34c4039faa7a88e11c7eecba7dbe2b706", size = 86334, upload-time = "2025-11-20T09:41:44.062Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, + { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, + { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, + { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, + { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, + { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, + { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, + { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, + { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, + { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, + { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, + { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, + { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, + { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, + { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, + { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, + { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, + { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, + { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, + { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, + { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[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, 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, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[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, 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, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[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/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { 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 = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[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, 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, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[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, 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, 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, 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, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] diff --git a/webapp/__init__.py b/webapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/webapp/forge.py b/webapp/forge.py new file mode 100644 index 0000000..68aa5f7 --- /dev/null +++ b/webapp/forge.py @@ -0,0 +1,115 @@ +""" +Forgejo API client and git operations for ifc-commit. + +All network and git calls that would block the event loop are exposed as +synchronous functions meant to be run in a thread pool via +asyncio.run_in_executor(). +""" + +import base64 +import os +import subprocess +import httpx + +FORGE_BASE = "https://gitaec.org" + + +# --------------------------------------------------------------------------- +# Forgejo API +# --------------------------------------------------------------------------- + +async def list_yaml_files(repo: str, branch: str, token: str) -> list[str]: + """ + List yaml files inside the yaml/ directory of a Forgejo repository. + Returns filenames only (e.g. ["duplex.yaml", "office.yaml"]). + """ + url = f"{FORGE_BASE}/api/v1/repos/{repo}/contents/yaml" + headers = {"Authorization": f"token {token}", "Accept": "application/json"} + async with httpx.AsyncClient() as client: + r = await client.get(url, params={"ref": branch}, headers=headers) + r.raise_for_status() + entries = r.json() + return [ + e["name"] for e in entries + if e.get("type") == "file" and e["name"].endswith((".yaml", ".yml")) + ] + + +async def get_file_content(repo: str, path: str, branch: str, token: str) -> str: + """ + Fetch the raw content of a file from a Forgejo repository. + Uses the /contents/ API (returns base64) to avoid bot-detection on /raw/. + Raises httpx.HTTPStatusError on non-2xx responses. + """ + url = f"{FORGE_BASE}/api/v1/repos/{repo}/contents/{path}" + headers = {"Authorization": f"token {token}", "Accept": "application/json"} + async with httpx.AsyncClient() as client: + r = await client.get(url, params={"ref": branch}, headers=headers) + r.raise_for_status() + data = r.json() + return base64.b64decode(data["content"]).decode() + + +# --------------------------------------------------------------------------- +# Git operations (synchronous — run in thread pool) +# --------------------------------------------------------------------------- + +def clone_repo_shallow(repo: str, token: str, workdir: str) -> None: + """Shallow clone (depth=1) — fast, no history, used for entity queries.""" + url = f"https://oauth2:{token}@{FORGE_BASE.removeprefix('https://')}/{repo}.git" + _run(["git", "clone", "--depth=1", url, workdir]) + + +def clone_repo(repo: str, token: str, workdir: str) -> None: + """ + Clone a Forgejo repository into workdir using token auth over HTTPS. + workdir must not exist yet (git clone creates it). + """ + url = f"https://oauth2:{token}@{FORGE_BASE.removeprefix('https://')}/{repo}.git" + _run(["git", "clone", url, workdir]) + + +def commit_and_push(workdir: str, message: str, author_name: str = "ifc-commit", author_email: str = "ifc-commit@gitaec.org") -> bool: + """ + Stage all changes, commit, and push in workdir. + Returns True if a commit was made, False if there was nothing to commit. + Raises subprocess.CalledProcessError on real git failures. + """ + env = { + **os.environ, + "GIT_AUTHOR_NAME": author_name, + "GIT_AUTHOR_EMAIL": author_email, + "GIT_COMMITTER_NAME": author_name, + "GIT_COMMITTER_EMAIL": author_email, + } + _run(["git", "add", "-A"], cwd=workdir, env=env) + + # Check if there is anything staged before committing + status = _run(["git", "status", "--porcelain"], cwd=workdir, env=env) + if not status.strip(): + return False # nothing to commit + + _run(["git", "commit", "-m", message], cwd=workdir, env=env) + _run(["git", "push"], cwd=workdir, env=env) + return True + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +def _run(cmd: list[str], cwd: str | None = None, env: dict | None = None) -> str: + """Run a subprocess, raise on non-zero exit, return combined stdout+stderr.""" + result = subprocess.run( + cmd, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + if result.returncode != 0: + raise subprocess.CalledProcessError( + result.returncode, cmd, output=result.stdout + ) + return result.stdout diff --git a/webapp/main.py b/webapp/main.py new file mode 100644 index 0000000..4976d1f --- /dev/null +++ b/webapp/main.py @@ -0,0 +1,299 @@ +""" +ifc-commit web app — FastAPI entry point. + +Routes: + GET / — serve index.html + GET /api/health — liveness check + POST /api/preview — fetch and parse a pipeline yaml from the forge + POST /api/run — clone → pipeline → commit/push, stream log lines +""" + +import asyncio +import contextlib +import io +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + +import httpx +import ifcopenshell +from fastapi import FastAPI, HTTPException +from fastapi.responses import FileResponse, StreamingResponse + +from webapp.schema import RepoRequest, EntityRequest, HistoryRequest +from webapp import forge, pipeline +from webapp.pipeline import has_runnable_ops + +app = FastAPI(title="ifc-commit") + + +def serve(): + """Entry point for the `ifccommit-web` script.""" + import uvicorn + uvicorn.run("webapp.main:app", host="127.0.0.1", port=8095, reload=True) + +# Only one pipeline run at a time — IFC files are large and CPU-heavy +_run_lock = asyncio.Lock() + +TEMPLATES_DIR = __file__.replace("main.py", "templates") + + +# --------------------------------------------------------------------------- +# Routes +# --------------------------------------------------------------------------- + +@app.get("/", response_class=FileResponse) +async def index(): + html = Path(__file__).parent.parent / "index.html" + if not html.exists(): + raise HTTPException(status_code=404, detail="Presentation page not built. Run: make html") + return FileResponse(str(html), media_type="text/html") + + +@app.get("/app", response_class=FileResponse) +async def app_ui(): + return FileResponse(f"{TEMPLATES_DIR}/index.html", media_type="text/html") + + +@app.get("/research", response_class=FileResponse) +async def research(): + html = Path(__file__).parent.parent / "research.html" + if not html.exists(): + raise HTTPException(status_code=404, detail="Research page not built. Run: make html") + return FileResponse(str(html), media_type="text/html") + + +@app.get("/api/health") +async def health(): + return {"status": "ok"} + + +@app.post("/api/yamls") +async def yamls(body: RepoRequest): + """List yaml pipeline files available in the yaml/ directory of the repo.""" + try: + files = await forge.list_yaml_files(body.repo, body.branch, body.token) + except httpx.HTTPStatusError as exc: + status = exc.response.status_code + if status == 404: + raise HTTPException(404, "yaml/ directory not found in repository") + raise HTTPException(status, f"Forge API error: {exc.response.text}") + return {"files": files} + + +@app.post("/api/preview") +async def preview(body: RepoRequest): + """Fetch a pipeline yaml from the forge and return the parsed operations list.""" + try: + raw = await forge.get_file_content( + body.repo, body.yaml_file, body.branch, body.token + ) + except httpx.HTTPStatusError as exc: + status = exc.response.status_code + if status == 404: + raise HTTPException(404, f"{body.yaml_file} not found in repository") + raise HTTPException(status, f"Forge API error: {exc.response.text}") + + try: + config = pipeline.parse_yaml(raw) + except Exception as exc: + raise HTTPException(422, f"Invalid {body.yaml_file}: {exc}") + + return {"operations": [op.model_dump() for op in config.operations]} + + +@app.post("/api/entities") +async def entities(body: EntityRequest): + """Shallow-clone the repo and return entity names for the given IFC type.""" + loop = asyncio.get_running_loop() + tmpdir = tempfile.mkdtemp(prefix="ifccommit_ent_") + repo_dir = f"{tmpdir}/repo" + try: + await loop.run_in_executor(None, forge.clone_repo_shallow, body.repo, body.token, repo_dir) + import ifcopenshell.util.selector + ifc_path = os.path.join(repo_dir, body.file) + model = ifcopenshell.open(ifc_path) + if body.location: + elements = ifcopenshell.util.selector.filter_elements( + model, f'IfcElement, location = "{body.location}"' + ) + names = sorted(e.Name for e in elements if e.Name) + else: + names = sorted(e.Name for e in model.by_type(body.ifc_type) if e.Name) + return {"names": names} + except subprocess.CalledProcessError as exc: + raise HTTPException(500, f"Clone failed: {exc.output}") + except Exception as exc: + raise HTTPException(500, str(exc)) + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + +@app.post("/api/ifc-history") +async def ifc_history(body: HistoryRequest): + """Shallow-clone the repo, read ifc/history.json, and return the records.""" + import json as _json + loop = asyncio.get_running_loop() + tmpdir = tempfile.mkdtemp(prefix="ifccommit_hist_") + repo_dir = f"{tmpdir}/repo" + try: + await loop.run_in_executor( + None, forge.clone_repo_shallow, body.repo, body.token, repo_dir + ) + json_path = os.path.join(repo_dir, body.file) + if not os.path.exists(json_path): + return {"records": [], "repo": body.repo, "branch": body.branch} + with open(json_path) as f: + records = _json.load(f) + return {"records": records, "repo": body.repo, "branch": body.branch} + except subprocess.CalledProcessError as exc: + raise HTTPException(500, f"Clone failed: {exc.output}") + except HTTPException: + raise + except Exception as exc: + raise HTTPException(500, str(exc)) + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + +@app.post("/api/run") +async def run(body: RepoRequest): + """ + Clone the repository, run all operations from the pipeline yaml, + commit the results, and push back to the forge. + Streams newline-terminated log lines to the client in real time. + """ + return StreamingResponse(_run_stream(body), media_type="text/plain") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +class _LogWriter: + """ + A file-like object that splits writes on newlines and forwards each + complete line to a log callback. Used to capture print() from cmd_*. + """ + def __init__(self, log_fn): + self._log = log_fn + self._buf = "" + + def write(self, s: str) -> None: + self._buf += s + while "\n" in self._buf: + line, self._buf = self._buf.split("\n", 1) + if line: + self._log(line) + + def flush(self) -> None: + if self._buf.strip(): + self._log(self._buf) + self._buf = "" + + +# --------------------------------------------------------------------------- +# Streaming pipeline +# --------------------------------------------------------------------------- + +async def _run_stream(body: RepoRequest): + """Async generator that runs the full pipeline and yields log lines.""" + + if _run_lock.locked(): + yield "ERROR: another run is already in progress — try again shortly\n" + return + + async with _run_lock: + loop = asyncio.get_running_loop() + tmpdir = tempfile.mkdtemp(prefix="ifccommit_") + # git clone creates its own directory — give it a path that doesn't exist yet + repo_dir = f"{tmpdir}/repo" + + try: + # 1. Fetch and parse pipeline yaml + yield f"Fetching {body.yaml_file}...\n" + try: + raw = await forge.get_file_content( + body.repo, body.yaml_file, body.branch, body.token + ) + except httpx.HTTPStatusError as exc: + yield f"ERROR: could not fetch {body.yaml_file} ({exc.response.status_code})\n" + return + + try: + config = pipeline.parse_yaml(raw) + except Exception as exc: + yield f"ERROR: invalid {body.yaml_file} — {exc}\n" + return + + yield f"Found {len(config.operations)} operation(s)\n" + + if not has_runnable_ops(config): + yield "No runnable operations — load history via the panel below.\n" + return + + # 2. Clone the repository + yield "Cloning repository...\n" + try: + await loop.run_in_executor( + None, forge.clone_repo, body.repo, body.token, repo_dir + ) + except subprocess.CalledProcessError as exc: + yield f"ERROR: git clone failed\n{exc.output}\n" + return + yield "Clone OK\n" + + # 3. Run the pipeline (synchronous — in thread pool). + # Log lines AND stdout from cmd_* functions are forwarded to the + # client via an asyncio.Queue. + queue: asyncio.Queue[str | None] = asyncio.Queue() + + def log(line: str) -> None: + loop.call_soon_threadsafe(queue.put_nowait, line + "\n") + + def _run() -> bool: + # Capture print() output from cmd_* and forward to log. + # Safe because _run_lock ensures only one thread does this. + writer = _LogWriter(log) + with contextlib.redirect_stdout(writer): + ok = pipeline.run_pipeline(config, repo_dir, log, body.overrides, body.id_overrides, body.element_overrides, yaml_path=body.yaml_file) + writer.flush() + loop.call_soon_threadsafe(queue.put_nowait, None) # sentinel + return ok + + future = loop.run_in_executor(None, _run) + + while True: + line = await queue.get() + if line is None: + break + yield line + + ok = await future + + if not ok: + yield "Pipeline completed with errors — skipping commit.\n" + return + + # 4. Commit and push results + yield "Committing results to forge...\n" + try: + committed = await loop.run_in_executor( + None, + forge.commit_and_push, + repo_dir, + "chore: ifccommit pipeline results [skip ci]", + ) + except subprocess.CalledProcessError as exc: + yield f"ERROR: git commit/push failed\n{exc.output}\n" + return + + if committed: + yield "Done — results committed and pushed.\n" + else: + yield "Done — no changes to commit (outputs unchanged).\n" + + finally: + shutil.rmtree(tmpdir, ignore_errors=True) diff --git a/webapp/pipeline.py b/webapp/pipeline.py new file mode 100644 index 0000000..196b861 --- /dev/null +++ b/webapp/pipeline.py @@ -0,0 +1,267 @@ +""" +Pipeline: parse ifccommit.yaml and execute IFC operations by calling +ifccommit.cmd_* functions directly. +""" + +import argparse +import os +import sys +from typing import Callable + +import yaml + +# ifccommit.py lives at the repo root — ensure it is importable +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +import ifccommit + +from webapp.schema import IfcCommitYaml, Operation + + +def parse_yaml(raw: str) -> IfcCommitYaml: + """Parse raw YAML text into a validated IfcCommitYaml model.""" + data = yaml.safe_load(raw) + return IfcCommitYaml.model_validate(data) + + +def has_runnable_ops(config: IfcCommitYaml) -> bool: + """Return True if the pipeline has operations that modify IFC files. + Pure history-read pipelines (ReadHistory only) are display-only and don't run. + """ + return any( + op.command not in ("history", "list", "info") or op.write_psets + for op in config.operations + ) + + +def run_pipeline( + config: IfcCommitYaml, + workdir: str, + log: Callable[[str], None], + overrides: dict[str, str] | None = None, + id_overrides: dict[str, str] | None = None, + element_overrides: dict[str, str] | None = None, + yaml_path: str | None = None, +) -> bool: + """ + Execute all operations in config against files in workdir. + Calls log(line) for each status/error line. + overrides maps op name → ifc_type (applied to info operations). + Returns True if all operations succeeded, False if any failed. + """ + overrides = overrides or {} + id_overrides = id_overrides or {} + element_overrides = element_overrides or {} + all_ok = True + + for op in config.operations: + if op.command == "info" and op.name in overrides: + op = op.model_copy(update={"ifc_type": overrides[op.name]}) + if op.command == "extract" and op.id and op.name in id_overrides: + op = op.model_copy(update={"id": id_overrides[op.name]}) + if ( + op.command == "modify" + and op.name in element_overrides + and element_overrides[op.name] + ): + op = op.model_copy(update={"element": element_overrides[op.name]}) + log(f"--- [{op.name}] command={op.command} ---") + try: + ns = _build_namespace(op, workdir, yaml_path=yaml_path) + _dispatch(op, ns) + log(f" OK") + except Exception as exc: + log(f" ERROR: {exc}") + all_ok = False + + return all_ok + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _abs(workdir: str, path: str | None) -> str | None: + """Resolve a repo-relative path to an absolute path inside workdir.""" + if path is None: + return None + if os.path.isabs(path): + return path + return os.path.join(workdir, path) + + +def _build_namespace( + op: Operation, workdir: str, yaml_path: str | None = None +) -> argparse.Namespace: + """ + Translate an Operation into the argparse.Namespace that the matching + cmd_* function expects. + """ + cmd = op.command + + if cmd == "list": + return argparse.Namespace(input=_abs(workdir, op.input)) + + if cmd == "info": + return argparse.Namespace( + input=_abs(workdir, op.input), + ifc_type=op.ifc_type, + ) + + if cmd == "extract": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + if op.id: + # Specific space extraction: extract type=IfcSpace id=A102 + return argparse.Namespace( + input=_abs(workdir, op.input), + output=_abs(workdir, op.output), + name=op.id, + by="name", + ) + return argparse.Namespace( + input=_abs(workdir, op.input), + output=_abs(workdir, op.output), + ifc_types=[op.query] if op.query else op.types, + ) + + if cmd == "insert": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + return argparse.Namespace( + base=_abs(workdir, op.base), + part=_abs(workdir, op.part), + output=_abs(workdir, op.output), + ) + + if cmd == "replace": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + return argparse.Namespace( + base=_abs(workdir, op.base), + space=op.space, + part=_abs(workdir, op.part), + output=_abs(workdir, op.output), + ) + + if cmd == "split": + return argparse.Namespace( + input=_abs(workdir, op.input), + outdir=_abs(workdir, op.outdir), + ifc_types=op.types or [], + ) + + if cmd == "space": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + return argparse.Namespace( + input=_abs(workdir, op.input), + output=_abs(workdir, op.output), + name=op.space, + by=op.by, + ) + + if cmd == "move": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + return argparse.Namespace( + input=_abs(workdir, op.input), + output=_abs(workdir, op.output), + name=op.element, + entity_id=op.entity_id, + x=op.x, + y=op.y, + z=op.z, + ) + + if cmd == "copy": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + return argparse.Namespace( + input=_abs(workdir, op.input), + output=_abs(workdir, op.output), + entity_ids=op.entity_ids, + tags=op.tags, + x=op.x, + y=op.y, + z=op.z, + ) + + if cmd == "diff": + target_ifc = op.target if op.target else op.output + return argparse.Namespace( + source=_abs(workdir, op.input), + target=_abs(workdir, target_ifc), + verbose=op.verbose if hasattr(op, "verbose") else False, + output=_abs(workdir, op.output), + ) + + if cmd == "modify": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + return argparse.Namespace( + input=_abs(workdir, op.input), + output=_abs(workdir, op.output), + name=op.element, + entity_id=op.entity_id, + x=op.x, + y=op.y, + z=op.z, + ) + + if cmd == "merge": + outdir = os.path.dirname(_abs(workdir, op.output)) + os.makedirs(outdir, exist_ok=True) + return argparse.Namespace( + base=_abs(workdir, op.base), + space=op.space, + part=_abs(workdir, op.part), + output=_abs(workdir, op.output), + ) + + if cmd == "remove": + return argparse.Namespace( + input=_abs(workdir, op.input), + ifc_type=op.type, + output=_abs(workdir, op.output) if op.output else None, + ) + + if cmd == "history": + resolved_yaml = yaml_path or op.yaml_src or "yaml/duplex.yaml" + return argparse.Namespace( + input=_abs(workdir, op.input) if op.input else None, + output=_abs(workdir, op.output) if op.output else None, + write_psets=op.write_psets, + workdir=workdir, + yaml=resolved_yaml, + ) + + raise ValueError(f"Unknown command: {cmd}") + + +_DISPATCH = { + "list": ifccommit.cmd_list, + "info": ifccommit.cmd_info, + "extract": ifccommit.cmd_extract, + "insert": ifccommit.cmd_insert, + "replace": ifccommit.cmd_replace, + "split": ifccommit.cmd_split, + "space": ifccommit.cmd_space, + "move": ifccommit.cmd_move, + "copy": ifccommit.cmd_copy, + "diff": ifccommit.cmd_diff, + "history": ifccommit.cmd_history, + "remove": ifccommit.cmd_remove, +} + + +def _dispatch(op: Operation, ns: argparse.Namespace) -> None: + cmd = op.command + if cmd == "extract" and op.id: + ifccommit.cmd_space(ns) + elif cmd == "modify": + ifccommit.cmd_move(ns) + elif cmd == "merge": + ifccommit.cmd_replace(ns) + else: + _DISPATCH[cmd](ns) diff --git a/webapp/schema.py b/webapp/schema.py new file mode 100644 index 0000000..0d893e2 --- /dev/null +++ b/webapp/schema.py @@ -0,0 +1,196 @@ +""" +Pydantic models for ifccommit.yaml operation declarations and API payloads. +""" + +from __future__ import annotations +from typing import Literal, Optional +from pydantic import BaseModel, model_validator + + +# --------------------------------------------------------------------------- +# ifccommit.yaml — operation declarations +# --------------------------------------------------------------------------- + + +class Operation(BaseModel): + name: str + command: Literal[ + "list", + "info", + "extract", + "insert", + "replace", + "split", + "space", + "move", + "copy", + "diff", + "history", + "modify", + "merge", + "remove", + ] + + # Shared fields + input: Optional[str] = None + output: Optional[str] = None + + # extract (bulk) / split + types: Optional[list[str]] = None + + # extract (raw ifcopenshell selector query, used when types/id are insufficient) + query: Optional[str] = None + + # extract (specific): type + id + type: Optional[str] = None # singular IFC type (e.g. IfcSpace) + id: Optional[str] = None # space name (e.g. A102) + + # insert / replace / merge + base: Optional[str] = None + part: Optional[str] = None + + # replace / space / merge + space: Optional[str] = None + + # split + outdir: Optional[str] = None + + # info + ifc_type: Optional[str] = None + + # space + by: Literal["name", "longname"] = "name" + + # move / copy / modify + element: Optional[str] = None # element name substring + entity_id: Optional[int] = None + entity_ids: Optional[list[int]] = None + tags: Optional[list[str]] = None + x: float = 0.0 + y: float = 0.0 + z: float = 0.0 + + # modify / merge + tag: Optional[str] = None + + # diff + target: Optional[str] = None + verbose: bool = False + + # history + yaml_src: Optional[str] = None # path to the yaml file (e.g. yaml/duplex.yaml) + write_psets: bool = False # write mode: stamp Pset_GitCommit on output elements + + @model_validator(mode="after") + def check_required_fields(self) -> "Operation": + cmd = self.command + missing: list[str] = [] + + if cmd in ( + "list", + "extract", + "split", + "space", + "move", + "copy", + "diff", + "modify", + ): + if not self.input: + missing.append("input") + if cmd in ( + "extract", + "insert", + "replace", + "space", + "move", + "copy", + "modify", + "merge", + ): + if not self.output: + missing.append("output") + if cmd == "extract": + if not self.types and not self.query and not (self.type and self.id): + missing.append("types (bulk), query, or type+id (specific)") + if cmd in ("insert",): + if not self.base: + missing.append("base") + if not self.part: + missing.append("part") + if cmd in ("replace", "merge"): + if not self.base: + missing.append("base") + if not self.space: + missing.append("space") + if not self.part: + missing.append("part") + if cmd == "split": + if not self.outdir: + missing.append("outdir") + if cmd == "space": + if not self.space: + missing.append("space") + if cmd == "remove": + if not self.input: + missing.append("input") + if not self.type: + missing.append("type") + if cmd == "info": + if not self.input: + missing.append("input") + if not self.ifc_type: + missing.append("ifc_type") + if cmd == "history": + if self.input and not self.output: + missing.append("output (required when input is set)") + if cmd == "copy": + if not self.input: + missing.append("input") + if not self.output: + missing.append("output") + if not self.entity_ids and not self.tags: + missing.append("entity_ids or tags") + + if missing: + raise ValueError( + f"Operation '{self.name}' (command={cmd}) is missing required fields: {missing}" + ) + return self + + +class IfcCommitYaml(BaseModel): + src: Optional[str] = None + operations: list[Operation] = [] + dest: Optional[str] = None + + +# --------------------------------------------------------------------------- +# API payloads +# --------------------------------------------------------------------------- + + +class RepoRequest(BaseModel): + repo: str # e.g. "orgname/reponame" + branch: str = "main" + token: str + yaml_file: str = "yaml/duplex.yaml" # repo-relative path to the pipeline yaml + overrides: dict[str, str] = {} # op name → ifc_type (for info ops) + id_overrides: dict[str, str] = {} # op name → id (for extract ops) + element_overrides: dict[str, str] = {} # op name → element (for modify ops) + + +class EntityRequest(BaseModel): + repo: str + branch: str = "main" + token: str + file: str # repo-relative path to the IFC file + ifc_type: str # IFC type to list (e.g. IfcSpace) + location: Optional[str] = None # if set, filter IfcElement by this space name + + +class HistoryRequest(BaseModel): + repo: str + branch: str = "main" + token: str + file: str = "ifc/history.json" # repo-relative path to the history JSON