refactor(python): rename package from 'contexa' to 'cortexa'

Swadhin Biswas committed 7 months ago
3fa717d
1 parent 7cbf4e3
commit 3fa717d
13 files changed
+699 -455
▼PYTHON/README.md
+21-21
1
-# contexa
1
+# cortexa
2
2
3
-[![PyPI version](https://img.shields.io/pypi/v/contexa.svg)](https://pypi.org/project/contexa/)
4
-[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
5
-[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/swadhinbiswas/contexa/blob/main/LICENSE)
6
-[![GitHub](https://img.shields.io/badge/GitHub-swadhinbiswas%2Fcontexa-black.svg?logo=github)](https://github.com/swadhinbiswas/contexa)
3
+[![PyPI version](https://img.shields.io/pypi/v/cortexa.svg)](https://pypi.org/project/cortexa/)
4
+[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
5
+[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/swadhinbiswas/Cortexa/blob/main/LICENSE)
6
+[![GitHub](https://img.shields.io/badge/GitHub-swadhinbiswas%2FCortexa-black.svg?logo=github)](https://github.com/swadhinbiswas/Cortexa)
7
7
8
-A Python implementation of the **contexa** framework -- Git-inspired context management for LLM agents.
8
+A Python implementation of the **cortexa** framework -- Git-inspired context management for LLM agents.
9
9
10
10
Based on: [arXiv:2508.00031](https://arxiv.org/abs/2508.00031) -- *"Git Context Controller: Manage the Context of LLM-based Agents like Git"* (Junde Wu et al., 2025)
11
11
73
73
## Installation
74
74
75
75
```bash
76
-pip install contexa
76
+pip install cortexa
77
77
```
78
78
79
79
Or with [uv](https://docs.astral.sh/uv/):
80
80
81
81
```bash
82
-uv add contexa
82
+uv add cortexa
83
83
```
84
84
85
85
---
87
87
## Quick Start
88
88
89
89
```python
90
-from contexa import GCCWorkspace
90
+from cortexa import GCCWorkspace
91
91
92
92
# 1. Initialize a workspace
93
93
ws = GCCWorkspace("/path/to/project")
284
284
All models support serialization:
285
285
286
286
```python
287
-from contexa import OTARecord, BranchMetadata
287
+from cortexa import OTARecord, BranchMetadata
288
288
289
289
# OTARecord <-> dict
290
290
record = OTARecord.from_dict({"timestamp": "...", "observation": "...", ...})
302
302
303
303
## Real-World Example
304
304
305
-Here's how an autonomous coding agent might use contexa to manage its memory while building a web application:
305
+Here's how an autonomous coding agent might use cortexa to manage its memory while building a web application:
306
306
307
307
```python
308
-from contexa import GCCWorkspace
308
+from cortexa import GCCWorkspace
309
309
310
310
ws = GCCWorkspace("./my-webapp")
311
311
ws.init("Build a Flask web app with user auth, blog posts, and admin panel")
348
348
349
349
```bash
350
350
# Clone the repository
351
-git clone https://github.com/swadhinbiswas/contexa.git
352
-cd contexa
351
+git clone https://github.com/swadhinbiswas/Cortexa.git
352
+cd cortexa
353
353
354
354
# Install dev dependencies and run tests
355
355
uv sync
380
380
381
381
Contributions are welcome! Here's how to get started:
382
382
383
-1. Fork the repository: [https://github.com/swadhinbiswas/contexa](https://github.com/swadhinbiswas/contexa)
383
+1. Fork the repository: [https://github.com/swadhinbiswas/Cortexa](https://github.com/swadhinbiswas/Cortexa)
384
384
2. Create a feature branch: `git checkout -b feature/my-feature`
385
385
3. Make your changes and add tests
386
386
4. Run the test suite: `uv run pytest -v`
387
387
5. Submit a pull request
388
388
389
-Please open an [issue](https://github.com/swadhinbiswas/contexa/issues) first for major changes to discuss the approach.
389
+Please open an [issue](https://github.com/swadhinbiswas/Cortexa/issues) first for major changes to discuss the approach.
390
390
391
391
---
392
392
393
393
## Requirements
394
394
395
-- **Python** >= 3.12
395
+- **Python** >= 3.10
396
396
- **PyYAML** >= 6.0
397
397
398
398
No other dependencies. The entire implementation uses Python's standard library (`dataclasses`, `pathlib`, `uuid`, `datetime`) plus PyYAML for metadata serialization.
401
401
402
402
## License
403
403
404
-This project is licensed under the MIT License. See the [LICENSE](https://github.com/swadhinbiswas/contexa/blob/main/LICENSE) file for details.
404
+This project is licensed under the MIT License. See the [LICENSE](https://github.com/swadhinbiswas/Cortexa/blob/main/LICENSE) file for details.
405
405
406
406
---
407
407
422
422
423
423
## Links
424
424
425
-- **GitHub Repository**: [https://github.com/swadhinbiswas/contexa](https://github.com/swadhinbiswas/contexa)
426
-- **PyPI Package**: [https://pypi.org/project/contexa/](https://pypi.org/project/contexa/)
427
-- **Issue Tracker**: [https://github.com/swadhinbiswas/contexa/issues](https://github.com/swadhinbiswas/contexa/issues)
425
+- **GitHub Repository**: [https://github.com/swadhinbiswas/Cortexa](https://github.com/swadhinbiswas/Cortexa)
426
+- **PyPI Package**: [https://pypi.org/project/cortexa/](https://pypi.org/project/cortexa/)
427
+- **Issue Tracker**: [https://github.com/swadhinbiswas/Cortexa/issues](https://github.com/swadhinbiswas/Cortexa/issues)
428
428
- **Original Paper**: [arXiv:2508.00031v2](https://arxiv.org/abs/2508.00031v2)
429
429
- **Author**: [Swadhin Biswas](https://github.com/swadhinbiswas)
430
430
▼PYTHON/contexa/workspace.py
+0-407
1
-"""
2
-GCC Workspace — core file-system layer.
3
-Based on arXiv:2508.00031v2.
4
-
5
-The paper specifies a .GCC/ root directory containing:
6
- main.md — global roadmap / planning artifact
7
- branches/
8
- <branch_name>/
9
- log.md — continuous OTA (Observation-Thought-Action) trace
10
- commit.md — milestone-level commit summaries
11
- metadata.yaml — branch intent, status, creation info
12
-
13
-All four commands (COMMIT, BRANCH, MERGE, CONTEXT) operate on this structure.
14
-"""
15
-
16
-import os
17
-import uuid
18
-from datetime import datetime, timezone
19
-from pathlib import Path
20
-from typing import Optional
21
-
22
-from .models import (
23
- BranchMetadata,
24
- CommitRecord,
25
- ContextResult,
26
- OTARecord,
27
-)
28
-
29
-
30
-MAIN_BRANCH = "main"
31
-GCC_DIR = ".GCC"
32
-
33
-
34
-def _now() -> str:
35
- return datetime.now(timezone.utc).isoformat()
36
-
37
-
38
-def _short_id() -> str:
39
- return str(uuid.uuid4())[:8]
40
-
41
-
42
-class GCCWorkspace:
43
- """
44
- Manages the .GCC/ directory structure for one agent project.
45
-
46
- Usage
47
- -----
48
- ws = GCCWorkspace("/path/to/project")
49
- ws.init("Build a REST API service")
50
-
51
- # agent logs OTA steps
52
- ws.log_ota("file list returned", "need to read main.py", "read_file main.py")
53
-
54
- # agent commits a milestone
55
- ws.commit("Implement GET /users endpoint")
56
-
57
- # agent branches to explore alternative
58
- ws.branch("auth-jwt", "Explore JWT-based auth vs session auth")
59
-
60
- # agent merges branch back
61
- ws.merge("auth-jwt")
62
-
63
- # retrieve context (K=1 by default, per paper experiments)
64
- ctx = ws.context(k=1)
65
- print(ctx.summary())
66
- """
67
-
68
- def __init__(self, project_root: str):
69
- self.root = Path(project_root)
70
- self.gcc_dir = self.root / GCC_DIR
71
- self._current_branch: str = MAIN_BRANCH
72
-
73
- def _branch_dir(self, branch: str) -> Path:
74
- return self.gcc_dir / "branches" / branch
75
-
76
- def _log_path(self, branch: str) -> Path:
77
- return self._branch_dir(branch) / "log.md"
78
-
79
- def _commit_path(self, branch: str) -> Path:
80
- return self._branch_dir(branch) / "commit.md"
81
-
82
- def _meta_path(self, branch: str) -> Path:
83
- return self._branch_dir(branch) / "metadata.yaml"
84
-
85
- def _main_md(self) -> Path:
86
- return self.gcc_dir / "main.md"
87
-
88
- def _read(self, path: Path) -> str:
89
- if path.exists():
90
- return path.read_text(encoding="utf-8")
91
- return ""
92
-
93
- def _append(self, path: Path, text: str) -> None:
94
- with open(path, "a", encoding="utf-8") as f:
95
- f.write(text)
96
-
97
- def _write(self, path: Path, text: str) -> None:
98
- path.parent.mkdir(parents=True, exist_ok=True)
99
- path.write_text(text, encoding="utf-8")
100
-
101
- def _read_commits(self, branch: str) -> list[CommitRecord]:
102
- """Parse commit.md into a list of CommitRecord objects."""
103
- text = self._read(self._commit_path(branch))
104
- records: list[CommitRecord] = []
105
- if not text:
106
- return records
107
- for block in text.split("---\n"):
108
- block = block.strip()
109
- if not block:
110
- continue
111
- lines = block.splitlines()
112
- commit_id = branch_purpose = prev_summary = contribution = ts = ""
113
- for line in lines:
114
- if line.startswith("## Commit"):
115
- commit_id = line.split("`")[1] if "`" in line else ""
116
- elif line.startswith("**Timestamp:**"):
117
- ts = line.split("**Timestamp:**")[-1].strip()
118
- elif line.startswith("**Branch Purpose:**"):
119
- branch_purpose = line.split("**Branch Purpose:**")[-1].strip()
120
- elif line.startswith("**Previous Progress Summary:**"):
121
- prev_summary = line.split("**Previous Progress Summary:**")[
122
- -1
123
- ].strip()
124
- elif line.startswith("**This Commit's Contribution:**"):
125
- contribution = line.split("**This Commit's Contribution:**")[
126
- -1
127
- ].strip()
128
-
129
- if commit_id:
130
- records.append(
131
- CommitRecord(
132
- commit_id=commit_id,
133
- branch_name=branch,
134
- branch_purpose=branch_purpose,
135
- previous_progress_summary=prev_summary,
136
- this_commit_contribution=contribution,
137
- timestamp=ts,
138
- )
139
- )
140
-
141
- return records
142
-
143
- def _read_ota(self, branch: str) -> list[OTARecord]:
144
- text = self._read(self._log_path(branch))
145
- records: list[OTARecord] = []
146
- if not text:
147
- return records
148
- for block in text.split("--------\n"):
149
- block = block.strip()
150
- if not block:
151
- continue
152
- lines = block.splitlines()
153
- step = 0
154
- ts = obs = thought = action = ""
155
- for line in lines:
156
- if line.startswith("### Step"):
157
- header = line.replace("### Step", "").strip()
158
- parts = header.split("-", 1)
159
- try:
160
- step = int(parts[0].strip())
161
- except ValueError:
162
- step = 0
163
- ts = parts[1].strip() if len(parts) > 1 else ""
164
- elif line.startswith("**Observation:**"):
165
- obs = line.split("**Observation:**")[-1].strip()
166
- elif line.startswith("**Thought:**"):
167
- thought = line.split("**Thought:**")[-1].strip()
168
- elif line.startswith("**Action:**"):
169
- action = line.split("**Action:**")[-1].strip()
170
- if obs or thought or action:
171
- records.append(OTARecord(ts, obs, thought, action, step))
172
-
173
- return records
174
-
175
- def init(self, project_roadmap: str = "") -> None:
176
- """
177
- Initialise a new GCC workspace. Creates .GCC/ structure with a
178
- 'main' branch, main.md roadmap, and initial metadata.
179
- """
180
- if self.gcc_dir.exists():
181
- raise FileExistsError(f"GCC workspace already exists at {self.gcc_dir}")
182
- main_branch_dir = self._branch_dir(MAIN_BRANCH)
183
- main_branch_dir.mkdir(parents=True)
184
- roadmap_content = (
185
- f"# Project Roadmap\n\n**Initialized:** {_now()}\n\n{project_roadmap}\n"
186
- )
187
- self._write(self._main_md(), roadmap_content)
188
- self._write(
189
- self._log_path(MAIN_BRANCH), f"# OTA Log — branch `{MAIN_BRANCH}`\n\n"
190
- )
191
- self._write(
192
- self._commit_path(MAIN_BRANCH),
193
- f"# Commit History — branch `{MAIN_BRANCH}`\n\n",
194
- )
195
-
196
- meta = BranchMetadata(
197
- name=MAIN_BRANCH,
198
- purpose="Primary reasoning trajectory",
199
- created_from="",
200
- created_at=_now(),
201
- status="active",
202
- )
203
- self._write(self._meta_path(MAIN_BRANCH), meta.to_yaml())
204
-
205
- self._current_branch = MAIN_BRANCH
206
-
207
- def load(self) -> None:
208
- """Load an existing GCC workspace (no-op if already in memory)."""
209
- if not self.gcc_dir.exists():
210
- raise FileNotFoundError(f"No GCC workspace found at {self.gcc_dir}")
211
- # Determine current branch from active metadata
212
- self._current_branch = MAIN_BRANCH
213
-
214
- def log_ota(self, observation: str, thought: str, action: str) -> OTARecord:
215
- """
216
- Append an OTA step to the current branch's log.md.
217
- The paper logs continuous OTA cycles: Observation-Thought-Action.
218
- """
219
- existing = self._read_ota(self._current_branch)
220
- step = len(existing) + 1
221
- record = OTARecord(
222
- timestamp=_now(),
223
- observation=observation,
224
- thought=thought,
225
- action=action,
226
- step=step,
227
- )
228
- self._append(self._log_path(self._current_branch), record.to_markdown())
229
- return record
230
-
231
- def commit(
232
- self,
233
- contribution: str,
234
- previous_summary: Optional[str] = None,
235
- update_roadmap: Optional[str] = None,
236
- ) -> CommitRecord:
237
- """
238
- COMMIT command (paper S3.2).
239
-
240
- Persists a milestone checkpoint to commit.md. Fields per paper:
241
- - Branch Purpose (from metadata)
242
- - Previous Progress Summary
243
- - This Commit's Contribution
244
-
245
- Optionally updates main.md global roadmap.
246
- """
247
- meta_text = self._read(self._meta_path(self._current_branch))
248
- meta = BranchMetadata.from_yaml(meta_text) if meta_text else None
249
- branch_purpose = meta.purpose if meta else ""
250
-
251
- # Auto-summarise previous progress from last commit if not provided
252
- if previous_summary is None:
253
- commits = self._read_commits(self._current_branch)
254
- if commits:
255
- previous_summary = commits[-1].this_commit_contribution
256
- else:
257
- previous_summary = "Initial state — no prior commits."
258
-
259
- record = CommitRecord(
260
- commit_id=_short_id(),
261
- branch_name=self._current_branch,
262
- branch_purpose=branch_purpose,
263
- previous_progress_summary=previous_summary,
264
- this_commit_contribution=contribution,
265
- timestamp=_now(),
266
- )
267
- self._append(self._commit_path(self._current_branch), record.to_markdown())
268
-
269
- # Optionally update the global roadmap in main.md
270
- if update_roadmap:
271
- self._append(
272
- self._main_md(),
273
- f"\n## Update ({record.timestamp})\n{update_roadmap}\n",
274
- )
275
-
276
- return record
277
-
278
- def branch(self, name: str, purpose: str) -> "GCCWorkspace":
279
- """
280
- BRANCH command (paper S3.3).
281
-
282
- Creates an isolated workspace for alternative plans or experimental
283
- reasoning. Mathematically: B_t^(name) = BRANCH(M_{t-1}).
284
-
285
- Initialises empty OTA trace (log.md) and commit.md for the branch,
286
- carrying forward the parent's last committed state as starting context.
287
- """
288
- branch_dir = self._branch_dir(name)
289
- if branch_dir.exists():
290
- raise FileExistsError(f"Branch '{name}' already exists.")
291
-
292
- branch_dir.mkdir(parents=True)
293
-
294
- # Initialise empty OTA log (fresh execution trace)
295
- self._write(self._log_path(name), f"# OTA Log — branch `{name}`\n\n")
296
- self._write(self._commit_path(name), f"# Commit History — branch `{name}`\n\n")
297
-
298
- # metadata.yaml records intent and motivation (paper S3.3)
299
- meta = BranchMetadata(
300
- name=name,
301
- purpose=purpose,
302
- created_from=self._current_branch,
303
- created_at=_now(),
304
- status="active",
305
- )
306
- self._write(self._meta_path(name), meta.to_yaml())
307
-
308
- # Switch current branch
309
- self._current_branch = name
310
- return self
311
-
312
- def merge(
313
- self,
314
- branch_name: str,
315
- summary: Optional[str] = None,
316
- target: str = MAIN_BRANCH,
317
- ) -> CommitRecord:
318
- """
319
- MERGE command (paper S3.4).
320
-
321
- Integrates a completed branch back into the target (default: main),
322
- merging summaries and execution traces to produce a unified,
323
- consistent state. Creates a merge commit on the target branch.
324
- """
325
- # Gather branch commits and synthesise
326
- branch_commits = self._read_commits(branch_name)
327
- branch_ota = self._read_ota(branch_name)
328
- meta_text = self._read(self._meta_path(branch_name))
329
- meta = BranchMetadata.from_yaml(meta_text) if meta_text else None
330
-
331
- if not summary:
332
- contributions = " | ".join(
333
- c.this_commit_contribution for c in branch_commits
334
- )
335
- summary = f"Merged branch `{branch_name}` ({len(branch_commits)} commits). Contributions: {contributions}"
336
-
337
- # Append branch OTA to target log (merge trace)
338
- if branch_ota:
339
- merge_header = f"\n## Merged from `{branch_name}` ({_now()})\n\n"
340
- self._append(self._log_path(target), merge_header)
341
- for rec in branch_ota:
342
- self._append(self._log_path(target), rec.to_markdown())
343
-
344
- # Create merge commit on target
345
- self._current_branch = target
346
- merge_commit = self.commit(
347
- contribution=summary,
348
- previous_summary=f"Merging branch `{branch_name}` with purpose: {meta.purpose if meta else ''}",
349
- update_roadmap=f"Merged `{branch_name}`: {summary}",
350
- )
351
-
352
- # Mark branch as merged in metadata
353
- if meta:
354
- meta.status = "merged"
355
- meta.merged_into = target
356
- meta.merged_at = _now()
357
- self._write(self._meta_path(branch_name), meta.to_yaml())
358
-
359
- return merge_commit
360
-
361
- def context(self, branch: Optional[str] = None, k: int = 1) -> ContextResult:
362
- """
363
- CONTEXT command (paper S3.5).
364
-
365
- Retrieves historical context at varying resolutions. The paper fixes
366
- K=1 in experiments (only the most recent commit record is revealed).
367
-
368
- Parameters
369
- ----------
370
- branch : branch to retrieve context from (default: current branch)
371
- k : number of recent commit records to surface (paper default K=1)
372
- """
373
- target = branch or self._current_branch
374
- commits = self._read_commits(target)
375
- ota = self._read_ota(target)
376
- roadmap = self._read(self._main_md())
377
- meta_text = self._read(self._meta_path(target))
378
- meta = BranchMetadata.from_yaml(meta_text) if meta_text else None
379
-
380
- return ContextResult(
381
- branch_name=target,
382
- k=k,
383
- commits=commits[-k:] if commits else [],
384
- ota_records=ota,
385
- main_roadmap=roadmap,
386
- metadata=meta,
387
- )
388
-
389
- @property
390
- def current_branch(self) -> str:
391
- return self._current_branch
392
-
393
- def switch_branch(self, name: str) -> None:
394
- """Switch the active branch without creating a new one."""
395
- if not self._branch_dir(name).exists():
396
- raise FileNotFoundError(f"Branch '{name}' does not exist.")
397
- self._current_branch = name
398
-
399
- def list_branches(self) -> list[str]:
400
- branches_root = self.gcc_dir / "branches"
401
- if not branches_root.exists():
402
- return []
403
- return [d.name for d in branches_root.iterdir() if d.is_dir()]
404
-
405
- def update_roadmap(self, content: str) -> None:
406
- """Overwrite or append to main.md global roadmap."""
407
- self._append(self._main_md(), f"\n{content}\n")
408
0
▼PYTHON/contexa/.gitignore → PYTHON/cortexa/.gitignore
+0-0
0
0
▼PYTHON/contexa/.python-version → PYTHON/cortexa/.python-version
+0-0
0
0
▼PYTHON/contexa/README.md → PYTHON/cortexa/README.md
+0-0
0
0
▼PYTHON/contexa/__init__.py → PYTHON/cortexa/__init__.py
+1-1
1
1
"""
2
-gcc-context — Python implementation of Git-Context-Controller (GCC)
2
+cortexa — Python implementation of Git-Context-Controller (GCC)
3
3
4
4
Paper: "Git Context Controller: Manage the Context of LLM-based Agents like Git"
5
5
arXiv:2508.00031v2, Junde Wu et al., 2025
6
6
▼PYTHON/contexa/main.py → PYTHON/cortexa/main.py
+1-1
1
1
def main():
2
- print("Hello from contexa!")
2
+ print("Hello from cortexa!")
3
3
4
4
5
5
if __name__ == "__main__":
6
6
▼PYTHON/contexa/models.py → PYTHON/cortexa/models.py
+46-8
9
9
10
10
"""
11
11
12
+from __future__ import annotations
13
+
12
14
from dataclasses import dataclass, field
13
15
from datetime import datetime
14
16
from typing import Dict, Optional
15
17
import yaml
16
18
17
19
20
+def _sanitize(text: str) -> str:
21
+ """Escape separator sequences in user-provided content.
22
+
23
+ Lines that are exactly ``---`` (the markdown/OTA block separator)
24
+ would break all parsers that split on ``---\\n``. We prepend a
25
+ backslash so the separator is preserved in the stored markdown
26
+ without confusing the splitter.
27
+ """
28
+ return text.replace("\n---\n", "\n\\---\n")
29
+
30
+
31
+def _desanitize(text: str) -> str:
32
+ """Reverse the escaping applied by :func:`_sanitize`."""
33
+ return text.replace("\n\\---\n", "\n---\n")
34
+
35
+
36
+def _split_blocks(text: str) -> list[str]:
37
+ """Split markdown text on ``---\\n`` while respecting escaped separators.
38
+
39
+ After naive splitting on ``---\\n``, blocks whose predecessor ends
40
+ with ``\\`` are rejoined (the backslash means ``\\---\\n`` was an
41
+ escaped separator produced by :func:`_sanitize`).
42
+ """
43
+ raw = text.split("---\n")
44
+ blocks: list[str] = []
45
+ i = 0
46
+ while i < len(raw):
47
+ block = raw[i]
48
+ while block.endswith("\\") and i + 1 < len(raw):
49
+ i += 1
50
+ block = block + "---\n" + raw[i]
51
+ blocks.append(block)
52
+ i += 1
53
+ return blocks
54
+
55
+
18
56
@dataclass
19
57
class OTARecord:
20
58
"""
30
68
31
69
def to_markdown(self) -> str:
32
70
return (
33
- f"### Step {self.step}-{self.timestamp}\n"
34
- f"**Observation:** {self.observation}\n\n"
35
- f"**Thought:** {self.thought}\n\n"
36
- f"**Action:** {self.action}\n\n"
37
- "--------\n"
71
+ f"### Step {self.step} — {self.timestamp}\n"
72
+ f"**Observation:** {_sanitize(self.observation)}\n\n"
73
+ f"**Thought:** {_sanitize(self.thought)}\n\n"
74
+ f"**Action:** {_sanitize(self.action)}\n\n"
75
+ "---\n"
38
76
)
39
77
40
78
@staticmethod
61
99
return (
62
100
f"## Commit `{self.commit_id}`\n"
63
101
f"**Timestamp:** {self.timestamp}\n\n"
64
- f"**Branch Purpose:** {self.branch_purpose}\n\n"
65
- f"**Previous Progress Summary:** {self.previous_progress_summary}\n\n"
66
- f"**This Commit's Contribution:** {self.this_commit_contribution}\n\n"
102
+ f"**Branch Purpose:** {_sanitize(self.branch_purpose)}\n\n"
103
+ f"**Previous Progress Summary:** {_sanitize(self.previous_progress_summary)}\n\n"
104
+ f"**This Commit's Contribution:** {_sanitize(self.this_commit_contribution)}\n\n"
67
105
"---\n"
68
106
)
69
107
70
108
▼PYTHON/cortexa/workspace.py
+521-0
1
+"""
2
+GCC Workspace — core file-system layer.
3
+Based on arXiv:2508.00031v2.
4
+
5
+The paper specifies a .GCC/ root directory containing:
6
+ main.md — global roadmap / planning artifact
7
+ branches/
8
+ <branch_name>/
9
+ log.md — continuous OTA (Observation-Thought-Action) trace
10
+ commit.md — milestone-level commit summaries
11
+ metadata.yaml — branch intent, status, creation info
12
+
13
+All four commands (COMMIT, BRANCH, MERGE, CONTEXT) operate on this structure.
14
+"""
15
+
16
+from __future__ import annotations
17
+
18
+import fcntl
19
+import os
20
+import uuid
21
+from contextlib import contextmanager
22
+from datetime import datetime, timezone
23
+from pathlib import Path
24
+from typing import Iterator, Optional
25
+
26
+from .models import (
27
+ BranchMetadata,
28
+ CommitRecord,
29
+ ContextResult,
30
+ OTARecord,
31
+ _desanitize,
32
+ _split_blocks,
33
+)
34
+
35
+
36
+MAIN_BRANCH = "main"
37
+GCC_DIR = ".GCC"
38
+
39
+
40
+def _validate_not_empty(value: str, field: str) -> None:
41
+ """Raise ValueError if *value* is empty or whitespace-only."""
42
+ if not value or not value.strip():
43
+ raise ValueError(f"{field} must not be empty.")
44
+
45
+
46
+def _validate_branch_name(name: str) -> None:
47
+ """Raise ValueError if *name* is not a valid branch identifier."""
48
+ _validate_not_empty(name, "Branch name")
49
+ if "/" in name or "\\" in name:
50
+ raise ValueError(f"Branch name must not contain path separators: {name!r}")
51
+ if name in (".", ".."):
52
+ raise ValueError(f"Branch name must not be '.' or '..': {name!r}")
53
+
54
+
55
+def _now() -> str:
56
+ return datetime.now(timezone.utc).isoformat()
57
+
58
+
59
+def _short_id() -> str:
60
+ return str(uuid.uuid4())[:8]
61
+
62
+
63
+class GCCWorkspace:
64
+ """
65
+ Manages the .GCC/ directory structure for one agent project.
66
+
67
+ Usage
68
+ -----
69
+ ws = GCCWorkspace("/path/to/project")
70
+ ws.init("Build a REST API service")
71
+
72
+ # agent logs OTA steps
73
+ ws.log_ota("file list returned", "need to read main.py", "read_file main.py")
74
+
75
+ # agent commits a milestone
76
+ ws.commit("Implement GET /users endpoint")
77
+
78
+ # agent branches to explore alternative
79
+ ws.branch("auth-jwt", "Explore JWT-based auth vs session auth")
80
+
81
+ # agent merges branch back
82
+ ws.merge("auth-jwt")
83
+
84
+ # retrieve context (K=1 by default, per paper experiments)
85
+ ctx = ws.context(k=1)
86
+ print(ctx.summary())
87
+ """
88
+
89
+ def __init__(self, project_root: str):
90
+ self.root = Path(project_root)
91
+ self.gcc_dir = self.root / GCC_DIR
92
+ self._current_branch: str = MAIN_BRANCH
93
+
94
+ # ------------------------------------------------------------------ #
95
+ # File locking #
96
+ # ------------------------------------------------------------------ #
97
+
98
+ @contextmanager
99
+ def _lock(self) -> Iterator[None]:
100
+ """Acquire an exclusive file lock on .GCC/.lock for concurrency safety.
101
+
102
+ Uses POSIX ``fcntl.flock`` so multiple processes (or threads)
103
+ operating on the same workspace are serialised. The lock is
104
+ automatically released when the ``with`` block exits.
105
+ """
106
+ lock_path = self.gcc_dir / ".lock"
107
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
108
+ fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR)
109
+ try:
110
+ fcntl.flock(fd, fcntl.LOCK_EX)
111
+ yield
112
+ finally:
113
+ fcntl.flock(fd, fcntl.LOCK_UN)
114
+ os.close(fd)
115
+
116
+ def _branch_dir(self, branch: str) -> Path:
117
+ return self.gcc_dir / "branches" / branch
118
+
119
+ def _log_path(self, branch: str) -> Path:
120
+ return self._branch_dir(branch) / "log.md"
121
+
122
+ def _commit_path(self, branch: str) -> Path:
123
+ return self._branch_dir(branch) / "commit.md"
124
+
125
+ def _meta_path(self, branch: str) -> Path:
126
+ return self._branch_dir(branch) / "metadata.yaml"
127
+
128
+ def _main_md(self) -> Path:
129
+ return self.gcc_dir / "main.md"
130
+
131
+ def _read(self, path: Path) -> str:
132
+ if path.exists():
133
+ return path.read_text(encoding="utf-8")
134
+ return ""
135
+
136
+ def _append(self, path: Path, text: str) -> None:
137
+ with open(path, "a", encoding="utf-8") as f:
138
+ f.write(text)
139
+
140
+ def _write(self, path: Path, text: str) -> None:
141
+ path.parent.mkdir(parents=True, exist_ok=True)
142
+ path.write_text(text, encoding="utf-8")
143
+
144
+ def _read_commits(self, branch: str) -> list[CommitRecord]:
145
+ """Parse commit.md into a list of CommitRecord objects."""
146
+ text = self._read(self._commit_path(branch))
147
+ records: list[CommitRecord] = []
148
+ if not text:
149
+ return records
150
+ for block in _split_blocks(text):
151
+ block = block.strip()
152
+ if not block:
153
+ continue
154
+ lines = block.splitlines()
155
+ commit_id = ts = ""
156
+ fields: dict[str, list[str]] = {}
157
+ current: str | None = None
158
+ for line in lines:
159
+ if line.startswith("## Commit"):
160
+ commit_id = line.split("`")[1] if "`" in line else ""
161
+ current = None
162
+ elif line.startswith("**Timestamp:**"):
163
+ ts = line.split("**Timestamp:**")[-1].strip()
164
+ current = None
165
+ elif line.startswith("**Branch Purpose:**"):
166
+ val = line.split("**Branch Purpose:**")[-1].strip()
167
+ fields["branch_purpose"] = [val]
168
+ current = "branch_purpose"
169
+ elif line.startswith("**Previous Progress Summary:**"):
170
+ val = line.split("**Previous Progress Summary:**")[-1].strip()
171
+ fields["prev_summary"] = [val]
172
+ current = "prev_summary"
173
+ elif line.startswith("**This Commit's Contribution:**"):
174
+ val = line.split("**This Commit's Contribution:**")[-1].strip()
175
+ fields["contribution"] = [val]
176
+ current = "contribution"
177
+ elif current is not None and line.strip():
178
+ fields[current].append(line.strip())
179
+
180
+ if commit_id:
181
+
182
+ def _get(key: str) -> str:
183
+ return _desanitize("\n".join(fields.get(key, [""])).strip())
184
+
185
+ records.append(
186
+ CommitRecord(
187
+ commit_id=commit_id,
188
+ branch_name=branch,
189
+ branch_purpose=_get("branch_purpose"),
190
+ previous_progress_summary=_get("prev_summary"),
191
+ this_commit_contribution=_get("contribution"),
192
+ timestamp=ts,
193
+ )
194
+ )
195
+
196
+ return records
197
+
198
+ def _read_ota(self, branch: str) -> list[OTARecord]:
199
+ text = self._read(self._log_path(branch))
200
+ records: list[OTARecord] = []
201
+ if not text:
202
+ return records
203
+ for block in _split_blocks(text):
204
+ block = block.strip()
205
+ if not block:
206
+ continue
207
+ lines = block.splitlines()
208
+ step = 0
209
+ ts = ""
210
+ fields: dict[str, list[str]] = {}
211
+ current: str | None = None
212
+ for line in lines:
213
+ if line.startswith("### Step"):
214
+ header = line.replace("### Step", "").strip()
215
+ # Support both " — " (standard) and "-" (legacy) separators
216
+ if " — " in header:
217
+ parts = header.split(" — ", 1)
218
+ else:
219
+ parts = header.split("-", 1)
220
+ try:
221
+ step = int(parts[0].strip())
222
+ except ValueError:
223
+ step = 0
224
+ ts = parts[1].strip() if len(parts) > 1 else ""
225
+ current = None
226
+ elif line.startswith("**Observation:**"):
227
+ val = line.split("**Observation:**")[-1].strip()
228
+ fields["obs"] = [val]
229
+ current = "obs"
230
+ elif line.startswith("**Thought:**"):
231
+ val = line.split("**Thought:**")[-1].strip()
232
+ fields["thought"] = [val]
233
+ current = "thought"
234
+ elif line.startswith("**Action:**"):
235
+ val = line.split("**Action:**")[-1].strip()
236
+ fields["action"] = [val]
237
+ current = "action"
238
+ elif current is not None and line.strip():
239
+ fields[current].append(line.strip())
240
+
241
+ def _get(key: str) -> str:
242
+ return _desanitize("\n".join(fields.get(key, [""])).strip())
243
+
244
+ obs = _get("obs")
245
+ thought = _get("thought")
246
+ action = _get("action")
247
+ if obs or thought or action:
248
+ records.append(
249
+ OTARecord(
250
+ ts,
251
+ obs,
252
+ thought,
253
+ action,
254
+ step,
255
+ )
256
+ )
257
+
258
+ return records
259
+
260
+ def init(self, project_roadmap: str = "") -> None:
261
+ """
262
+ Initialise a new GCC workspace. Creates .GCC/ structure with a
263
+ 'main' branch, main.md roadmap, and initial metadata.
264
+ """
265
+ if self.gcc_dir.exists():
266
+ raise FileExistsError(f"GCC workspace already exists at {self.gcc_dir}")
267
+ main_branch_dir = self._branch_dir(MAIN_BRANCH)
268
+ main_branch_dir.mkdir(parents=True)
269
+
270
+ with self._lock():
271
+ roadmap_content = (
272
+ f"# Project Roadmap\n\n**Initialized:** {_now()}\n\n{project_roadmap}\n"
273
+ )
274
+ self._write(self._main_md(), roadmap_content)
275
+ self._write(
276
+ self._log_path(MAIN_BRANCH), f"# OTA Log — branch `{MAIN_BRANCH}`\n\n"
277
+ )
278
+ self._write(
279
+ self._commit_path(MAIN_BRANCH),
280
+ f"# Commit History — branch `{MAIN_BRANCH}`\n\n",
281
+ )
282
+
283
+ meta = BranchMetadata(
284
+ name=MAIN_BRANCH,
285
+ purpose="Primary reasoning trajectory",
286
+ created_from="",
287
+ created_at=_now(),
288
+ status="active",
289
+ )
290
+ self._write(self._meta_path(MAIN_BRANCH), meta.to_yaml())
291
+
292
+ self._current_branch = MAIN_BRANCH
293
+
294
+ def load(self) -> None:
295
+ """Load an existing GCC workspace (no-op if already in memory)."""
296
+ if not self.gcc_dir.exists():
297
+ raise FileNotFoundError(f"No GCC workspace found at {self.gcc_dir}")
298
+ # Determine current branch from active metadata
299
+ self._current_branch = MAIN_BRANCH
300
+
301
+ def log_ota(self, observation: str, thought: str, action: str) -> OTARecord:
302
+ """
303
+ Append an OTA step to the current branch's log.md.
304
+ The paper logs continuous OTA cycles: Observation-Thought-Action.
305
+ """
306
+ if (
307
+ not (observation and observation.strip())
308
+ and not (thought and thought.strip())
309
+ and not (action and action.strip())
310
+ ):
311
+ raise ValueError(
312
+ "At least one of observation, thought, or action must be non-empty."
313
+ )
314
+ with self._lock():
315
+ existing = self._read_ota(self._current_branch)
316
+ step = len(existing) + 1
317
+ record = OTARecord(
318
+ timestamp=_now(),
319
+ observation=observation,
320
+ thought=thought,
321
+ action=action,
322
+ step=step,
323
+ )
324
+ self._append(self._log_path(self._current_branch), record.to_markdown())
325
+ return record
326
+
327
+ def commit(
328
+ self,
329
+ contribution: str,
330
+ previous_summary: Optional[str] = None,
331
+ update_roadmap: Optional[str] = None,
332
+ ) -> CommitRecord:
333
+ """
334
+ COMMIT command (paper S3.2).
335
+
336
+ Persists a milestone checkpoint to commit.md. Fields per paper:
337
+ - Branch Purpose (from metadata)
338
+ - Previous Progress Summary
339
+ - This Commit's Contribution
340
+
341
+ Optionally updates main.md global roadmap.
342
+ """
343
+ _validate_not_empty(contribution, "Contribution")
344
+ with self._lock():
345
+ meta_text = self._read(self._meta_path(self._current_branch))
346
+ meta = BranchMetadata.from_yaml(meta_text) if meta_text else None
347
+ branch_purpose = meta.purpose if meta else ""
348
+
349
+ # Auto-summarise previous progress from last commit if not provided
350
+ if previous_summary is None:
351
+ commits = self._read_commits(self._current_branch)
352
+ if commits:
353
+ previous_summary = commits[-1].this_commit_contribution
354
+ else:
355
+ previous_summary = "Initial state — no prior commits."
356
+
357
+ record = CommitRecord(
358
+ commit_id=_short_id(),
359
+ branch_name=self._current_branch,
360
+ branch_purpose=branch_purpose,
361
+ previous_progress_summary=previous_summary,
362
+ this_commit_contribution=contribution,
363
+ timestamp=_now(),
364
+ )
365
+ self._append(self._commit_path(self._current_branch), record.to_markdown())
366
+
367
+ # Optionally update the global roadmap in main.md
368
+ if update_roadmap:
369
+ self._append(
370
+ self._main_md(),
371
+ f"\n## Update ({record.timestamp})\n{update_roadmap}\n",
372
+ )
373
+
374
+ return record
375
+
376
+ def branch(self, name: str, purpose: str) -> "GCCWorkspace":
377
+ """
378
+ BRANCH command (paper S3.3).
379
+
380
+ Creates an isolated workspace for alternative plans or experimental
381
+ reasoning. Mathematically: B_t^(name) = BRANCH(M_{t-1}).
382
+
383
+ Initialises empty OTA trace (log.md) and commit.md for the branch,
384
+ carrying forward the parent's last committed state as starting context.
385
+ """
386
+ _validate_branch_name(name)
387
+ _validate_not_empty(purpose, "Branch purpose")
388
+ branch_dir = self._branch_dir(name)
389
+ if branch_dir.exists():
390
+ raise FileExistsError(f"Branch '{name}' already exists.")
391
+
392
+ branch_dir.mkdir(parents=True)
393
+
394
+ with self._lock():
395
+ # Initialise empty OTA log (fresh execution trace)
396
+ self._write(self._log_path(name), f"# OTA Log — branch `{name}`\n\n")
397
+ self._write(
398
+ self._commit_path(name), f"# Commit History — branch `{name}`\n\n"
399
+ )
400
+
401
+ # metadata.yaml records intent and motivation (paper S3.3)
402
+ meta = BranchMetadata(
403
+ name=name,
404
+ purpose=purpose,
405
+ created_from=self._current_branch,
406
+ created_at=_now(),
407
+ status="active",
408
+ )
409
+ self._write(self._meta_path(name), meta.to_yaml())
410
+
411
+ # Switch current branch
412
+ self._current_branch = name
413
+ return self
414
+
415
+ def merge(
416
+ self,
417
+ branch_name: str,
418
+ summary: Optional[str] = None,
419
+ target: str = MAIN_BRANCH,
420
+ ) -> CommitRecord:
421
+ """
422
+ MERGE command (paper S3.4).
423
+
424
+ Integrates a completed branch back into the target (default: main),
425
+ merging summaries and execution traces to produce a unified,
426
+ consistent state. Creates a merge commit on the target branch.
427
+ """
428
+ _validate_branch_name(branch_name)
429
+ _validate_branch_name(target)
430
+ with self._lock():
431
+ # Gather branch commits and synthesise
432
+ branch_commits = self._read_commits(branch_name)
433
+ branch_ota = self._read_ota(branch_name)
434
+ meta_text = self._read(self._meta_path(branch_name))
435
+ meta = BranchMetadata.from_yaml(meta_text) if meta_text else None
436
+
437
+ if not summary:
438
+ contributions = " | ".join(
439
+ c.this_commit_contribution for c in branch_commits
440
+ )
441
+ summary = f"Merged branch `{branch_name}` ({len(branch_commits)} commits). Contributions: {contributions}"
442
+
443
+ # Append branch OTA to target log (merge trace)
444
+ if branch_ota:
445
+ merge_header = f"\n## Merged from `{branch_name}` ({_now()})\n\n"
446
+ self._append(self._log_path(target), merge_header)
447
+ for rec in branch_ota:
448
+ self._append(self._log_path(target), rec.to_markdown())
449
+
450
+ # Create merge commit on target
451
+ self._current_branch = target
452
+
453
+ # commit() acquires the lock internally
454
+ merge_commit = self.commit(
455
+ contribution=summary,
456
+ previous_summary=f"Merging branch `{branch_name}` with purpose: {meta.purpose if meta else ''}",
457
+ update_roadmap=f"Merged `{branch_name}`: {summary}",
458
+ )
459
+
460
+ # Mark branch as merged in metadata
461
+ with self._lock():
462
+ if meta:
463
+ meta.status = "merged"
464
+ meta.merged_into = target
465
+ meta.merged_at = _now()
466
+ self._write(self._meta_path(branch_name), meta.to_yaml())
467
+
468
+ return merge_commit
469
+
470
+ def context(self, branch: Optional[str] = None, k: int = 1) -> ContextResult:
471
+ """
472
+ CONTEXT command (paper S3.5).
473
+
474
+ Retrieves historical context at varying resolutions. The paper fixes
475
+ K=1 in experiments (only the most recent commit record is revealed).
476
+
477
+ Parameters
478
+ ----------
479
+ branch : branch to retrieve context from (default: current branch)
480
+ k : number of recent commit records to surface (paper default K=1)
481
+ """
482
+ if k < 1:
483
+ raise ValueError(f"k must be >= 1, got {k}")
484
+ target = branch or self._current_branch
485
+ with self._lock():
486
+ commits = self._read_commits(target)
487
+ ota = self._read_ota(target)
488
+ roadmap = self._read(self._main_md())
489
+ meta_text = self._read(self._meta_path(target))
490
+ meta = BranchMetadata.from_yaml(meta_text) if meta_text else None
491
+
492
+ return ContextResult(
493
+ branch_name=target,
494
+ k=k,
495
+ commits=commits[-k:] if commits else [],
496
+ ota_records=ota,
497
+ main_roadmap=roadmap,
498
+ metadata=meta,
499
+ )
500
+
501
+ @property
502
+ def current_branch(self) -> str:
503
+ return self._current_branch
504
+
505
+ def switch_branch(self, name: str) -> None:
506
+ """Switch the active branch without creating a new one."""
507
+ _validate_branch_name(name)
508
+ if not self._branch_dir(name).exists():
509
+ raise FileNotFoundError(f"Branch '{name}' does not exist.")
510
+ self._current_branch = name
511
+
512
+ def list_branches(self) -> list[str]:
513
+ branches_root = self.gcc_dir / "branches"
514
+ if not branches_root.exists():
515
+ return []
516
+ return [d.name for d in branches_root.iterdir() if d.is_dir()]
517
+
518
+ def update_roadmap(self, content: str) -> None:
519
+ """Overwrite or append to main.md global roadmap."""
520
+ with self._lock():
521
+ self._append(self._main_md(), f"\n{content}\n")
0
522
▼PYTHON/main.py
+0-6
1
-def main():
2
- print("Hello from python!")
3
-
4
-
5
-if __name__ == "__main__":
6
- main()
7
0
▼PYTHON/pyproject.toml
+9-6
1
1
[project]
2
-name = "contexa"
2
+name = "cortexa"
3
3
version = "0.1.1"
4
4
description = "Python implementation of Git-Context-Controller (GCC)"
5
5
readme = "README.md"
6
-requires-python = ">=3.12"
6
+requires-python = ">=3.10"
7
7
license = "MIT"
8
8
authors = [
9
9
{ name = "Swadhin Biswas", email = "[email protected]" },
14
14
"Intended Audience :: Developers",
15
15
"License :: OSI Approved :: MIT License",
16
16
"Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
17
19
"Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
18
21
"Topic :: Scientific/Engineering :: Artificial Intelligence",
19
22
]
20
23
dependencies = [
22
25
]
23
26
24
27
[project.urls]
25
-Homepage = "https://github.com/swadhinbiswas/contexa"
26
-Repository = "https://github.com/swadhinbiswas/contexa"
27
-Issues = "https://github.com/swadhinbiswas/contexa/issues"
28
+Homepage = "https://github.com/swadhinbiswas/Cortexa"
29
+Repository = "https://github.com/swadhinbiswas/Cortexa"
30
+Issues = "https://github.com/swadhinbiswas/Cortexa/issues"
28
31
29
32
[build-system]
30
33
requires = ["hatchling>=1.21"]
31
34
build-backend = "hatchling.build"
32
35
33
36
[tool.hatch.build.targets.wheel]
34
-packages = ["contexa"]
37
+packages = ["cortexa"]
35
38
36
39
[dependency-groups]
37
40
dev = [
38
41
▼PYTHON/test/test.py
+3-3
1
1
"""Tests for gcc-context Python package."""
2
2
3
3
import pytest
4
-from contexa import GCCWorkspace
4
+from cortexa import GCCWorkspace
5
5
6
6
7
7
@pytest.fixture
84
84
85
85
def test_branch_metadata_records_purpose(ws):
86
86
ws.branch("jwt-branch", "Test JWT auth approach")
87
- from contexa.models import BranchMetadata
87
+ from cortexa.models import BranchMetadata
88
88
89
89
meta_text = ws._read(ws._meta_path("jwt-branch"))
90
90
meta = BranchMetadata.from_yaml(meta_text)
97
97
ws.branch("to-merge", "Will be merged")
98
98
ws.commit("Branch work done")
99
99
ws.merge("to-merge", target="main")
100
- from contexa.models import BranchMetadata
100
+ from cortexa.models import BranchMetadata
101
101
102
102
meta_text = ws._read(ws._meta_path("to-merge"))
103
103
meta = BranchMetadata.from_yaml(meta_text)
104
104
▼PYTHON/uv.lock
+97-2
1
1
version = 1
2
2
revision = 3
3
-requires-python = ">=3.12"
3
+requires-python = ">=3.10"
4
4
5
5
[[package]]
6
6
name = "colorama"
12
12
]
13
13
14
14
[[package]]
15
-name = "contexa"
15
+name = "cortexa"
16
16
version = "0.1.1"
17
17
source = { editable = "." }
18
18
dependencies = [
30
30
[package.metadata.requires-dev]
31
31
dev = [{ name = "pytest", specifier = ">=9.0.2" }]
32
32
33
+[[package]]
34
+name = "exceptiongroup"
35
+version = "1.3.1"
36
+source = { registry = "https://pypi.org/simple" }
37
+dependencies = [
38
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
39
+]
40
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
41
+wheels = [
42
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
43
+]
44
+
33
45
[[package]]
34
46
name = "iniconfig"
35
47
version = "2.3.0"
72
84
source = { registry = "https://pypi.org/simple" }
73
85
dependencies = [
74
86
{ name = "colorama", marker = "sys_platform == 'win32'" },
87
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
75
88
{ name = "iniconfig" },
76
89
{ name = "packaging" },
77
90
{ name = "pluggy" },
78
91
{ name = "pygments" },
92
+ { name = "tomli", marker = "python_full_version < '3.11'" },
79
93
]
80
94
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" }
81
95
wheels = [
88
102
source = { registry = "https://pypi.org/simple" }
89
103
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" }
90
104
wheels = [
105
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
106
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
107
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
108
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
109
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
110
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
111
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
112
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
113
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
114
+ { 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" },
115
+ { 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" },
116
+ { 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" },
117
+ { 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" },
118
+ { 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" },
119
+ { 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" },
120
+ { 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" },
121
+ { 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" },
122
+ { 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" },
91
123
{ 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" },
92
124
{ 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" },
93
125
{ 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" },
127
159
{ 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" },
128
160
{ 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" },
129
161
]
162
+
163
+[[package]]
164
+name = "tomli"
165
+version = "2.4.0"
166
+source = { registry = "https://pypi.org/simple" }
167
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
168
+wheels = [
169
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
170
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
171
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
172
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
173
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
174
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
175
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
176
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
177
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
178
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
179
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
180
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
181
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
182
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
183
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
184
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
185
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
186
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
187
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
188
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
189
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
190
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
191
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
192
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
193
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
194
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
195
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
196
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
197
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
198
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
199
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
200
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
201
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
202
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
203
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
204
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
205
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
206
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
207
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
208
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
209
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
210
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
211
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
212
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
213
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
214
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
215
+]
216
+
217
+[[package]]
218
+name = "typing-extensions"
219
+version = "4.15.0"
220
+source = { registry = "https://pypi.org/simple" }
221
+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" }
222
+wheels = [
223
+ { 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" },
224
+]
130
225