release commit

This commit is contained in:
Milovann Yanatchkov 2026-03-25 10:36:30 +01:00
commit 56a7d320c5
38 changed files with 5252 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
__pycache__
tests/__pycache__
dist/
demo/
index.html
research.html

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.11

40
AGENT.md Normal file
View file

@ -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 <input.ifc> # list IFC types and counts
uv run ifccommit.py info <input.ifc> <IfcType> # dump element attributes
uv run ifccommit.py extract <input.ifc> <out.ifc> <IfcType> [...] # extract to new file
uv run ifccommit.py space <input.ifc> <out.ifc> <SpaceId> # extract space contents
uv run ifccommit.py split <input.ifc> <dist/> [type] # split by storey
uv run ifccommit.py move <input.ifc> <out.ifc> <id> --x <val> [--y] [--z] # translate element
uv run ifccommit.py insert <base.ifc> <part.ifc> <out.ifc> # merge files
uv run ifccommit.py replace <base.ifc> <spaceId> <part.ifc> <out.ifc> # replace space
```
## Testing
```bash
make test # run pytest
make test-pipeline # run full pipeline then pytest
```
## Important Notes
- Use `uv` for package management

21
LICENSE Normal file
View file

@ -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.

57
Makefile Normal file
View file

@ -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

146
README.md Normal file
View file

@ -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).

309
build_html.py Normal file
View file

@ -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 = """
<header class="site-header">
<div class="site-header-inner">
<a class="header-btn" href="/">ifc-commit</a>
<span class="header-tagline">Git-native workflows for IFC</span>
<nav>
<a href="/research">Research</a>
<a href="/app">Demo</a>
<a href="https://gitaec.org/rvba/ifc-commit" target="_blank">Source </a>
</nav>
</div>
</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 <strong>Example</strong>
body = re.sub(
r'<blockquote>\s*<p><strong>Example</strong>',
'<blockquote class="example"><p><span class="example-label">Example</span>',
body,
)
# Style blockquotes that open with <strong>Schema</strong>
body = re.sub(
r'<blockquote>\s*<p><strong>Schema</strong>',
'<blockquote class="schema"><p>Schema',
body,
)
# Protect headings from badge replacement
body = re.sub(
r'(<h[1-6][^>]*>)(.*?)(</h[1-6]>)',
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'(?<![=/"\w])ifc-commit(?![\w])', '<span class="badge">ifc-commit</span>', body)
# Restore headings
body = body.replace('\x00', 'ifc-commit')
# Turn the h1 into a title-button link
if title_link:
body = body.replace(
f'<h1>{title_text}</h1>',
f'<h1><a class="title-btn" href="{title_link}">{title_text}</a></h1>',
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"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>{CSS}</style>
</head>
<body>
{HEADER}
<div class="container">
{body}
</div>
</body>
</html>
"""
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",
)

2
docs/README.md Normal file
View file

@ -0,0 +1,2 @@
# ifc-commit

124
docs/architecture.md Normal file
View file

@ -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` | `<outdir>/<stem>_<StoreyName>.ifc` |
| `space` | user-specified (or `<stem>_<Name>.ifc` for multi-match) |
| `move` | user-specified |
| `copy` | user-specified |
| `diff` | stdout (or user-specified with `-o`) |
| `remove` | user-specified (or `<stem>_<type>_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/` |

248
docs/commands.md Normal file
View file

@ -0,0 +1,248 @@
# Commands
```bash
uv run ifccommit.py <command> [options]
```
## List
List all IFC types present in a file and their element counts.
```bash
uv run ifccommit.py list <input.ifc>
```
**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 <input.ifc> <IfcType>
```
**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 <input.ifc> <output.ifc> <IfcType|preset> [...]
```
**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 <base.ifc> <part.ifc> <output.ifc>
```
**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 <base.ifc> <space> <part.ifc> <output.ifc>
```
**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 <input.ifc> <outdir> [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 <input.ifc> <output.ifc> <name> [--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 <input.ifc> <output.ifc> [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 <input.ifc> <output.ifc> [--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 <source.ifc> <target.ifc> [-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 <file.ifc> [--output <out.json>]
```
**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 <input.ifc> <IfcType> [--output FILE]
```
**Options:**
- `--output` — Output IFC file (default: `<input>_<IfcType>_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
```

137
docs/history.md Normal file
View file

@ -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 <alice@example.com>` |
| `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.

133
docs/ifc-extract.md Normal file
View file

@ -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)

160
docs/ifcfile.md Normal file
View file

@ -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.

64
docs/install.md Normal file
View file

@ -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.

132
docs/merge.md Normal file
View file

@ -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).

152
docs/merge_native.md Normal file
View file

@ -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)

226
docs/research.md Normal file
View file

@ -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)

119
docs/space_modifications.md Normal file
View file

@ -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 <entity_id> --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
```

162
docs/wasm.md Normal file
View file

@ -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
<script src="https://cdn.jsdelivr.net/pyodide/v0.27/full/pyodide.js"></script>
```
```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 (~35 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

View file

@ -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;
}
}

View file

@ -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

View file

@ -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

39
files/yaml/duplex.yaml Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

12
files/yaml/office.yaml Normal file
View file

@ -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

907
ifccommit.py Normal file
View file

@ -0,0 +1,907 @@
#!/usr/bin/env python3
"""
ifc-commit slice, inspect, and compose IFC files.
Usage:
uv run ifccommit.py list <input.ifc>
uv run ifccommit.py info <input.ifc> <IfcType>
uv run ifccommit.py extract <input.ifc> <output.ifc> <IfcType|preset> [...]
uv run ifccommit.py insert <base.ifc> <part.ifc> <output.ifc>
uv run ifccommit.py replace <base.ifc> <space> <part.ifc> <output.ifc>
uv run ifccommit.py split <input.ifc> <outdir> [IfcType|preset ...]
uv run ifccommit.py space <input.ifc> <output.ifc> <name> [--by name|longname]
uv run ifccommit.py move <input.ifc> <output.ifc> [entity_id] [--name X] [--x N] [--y N] [--z N]
uv run ifccommit.py copy <input.ifc> <output.ifc> [--entity-ids N ...] [--tags X ...] [--x N] [--y N] [--z N]
uv run ifccommit.py diff <source.ifc> <target.ifc> [-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: <input>_<IfcType>_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()

28
pyproject.toml Normal file
View file

@ -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 = ["."]

View file

@ -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

49
scripts/release.sh Executable file
View file

@ -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."

70
scripts/reset_demo_repo.sh Executable file
View file

@ -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"

932
uv.lock generated Normal file
View file

@ -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" },
]

0
webapp/__init__.py Normal file
View file

115
webapp/forge.py Normal file
View file

@ -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

299
webapp/main.py Normal file
View file

@ -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)

267
webapp/pipeline.py Normal file
View file

@ -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)

196
webapp/schema.py Normal file
View file

@ -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