agentic-data-contracts 0.9.1__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/CHANGELOG.md +10 -0
  2. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/PKG-INFO +86 -65
  3. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/README.md +74 -53
  4. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/docs/architecture.md +2 -2
  5. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/pyproject.toml +11 -9
  6. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/core/session.py +13 -1
  7. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_session.py +59 -0
  8. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/uv.lock +30 -30
  9. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/.github/dependabot.yml +0 -0
  10. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/.github/workflows/ci.yml +0 -0
  11. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/.gitignore +0 -0
  12. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/.pre-commit-config.yaml +0 -0
  13. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/.python-version +0 -0
  14. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/CLAUDE.md +0 -0
  15. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/LICENSE +0 -0
  16. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/examples/revenue_agent/agent.py +0 -0
  17. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/examples/revenue_agent/contract.yml +0 -0
  18. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/examples/revenue_agent/semantic.yml +0 -0
  19. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/examples/revenue_agent/setup_db.py +0 -0
  20. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/__init__.py +0 -0
  21. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/adapters/__init__.py +0 -0
  22. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/adapters/_normalizer.py +0 -0
  23. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/adapters/base.py +0 -0
  24. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/adapters/duckdb.py +0 -0
  25. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/bridge/__init__.py +0 -0
  26. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/bridge/compiler.py +0 -0
  27. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/core/__init__.py +0 -0
  28. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/core/contract.py +0 -0
  29. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/core/prompt.py +0 -0
  30. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/core/schema.py +0 -0
  31. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/py.typed +0 -0
  32. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/semantic/__init__.py +0 -0
  33. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/semantic/base.py +0 -0
  34. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/semantic/cube.py +0 -0
  35. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/semantic/dbt.py +0 -0
  36. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/semantic/yaml_source.py +0 -0
  37. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/tools/__init__.py +0 -0
  38. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/tools/factory.py +0 -0
  39. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/tools/middleware.py +0 -0
  40. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/tools/sdk.py +0 -0
  41. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/validation/__init__.py +0 -0
  42. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/validation/checkers.py +0 -0
  43. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/validation/explain.py +0 -0
  44. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/src/agentic_data_contracts/validation/validator.py +0 -0
  45. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/__init__.py +0 -0
  46. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/conftest.py +0 -0
  47. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/fixtures/minimal_contract.yml +0 -0
  48. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/fixtures/relationships_checker.yml +0 -0
  49. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/fixtures/sample_cube_schema.yml +0 -0
  50. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/fixtures/sample_dbt_manifest.json +0 -0
  51. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/fixtures/semantic_source.yml +0 -0
  52. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/fixtures/valid_contract.yml +0 -0
  53. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_adapters/__init__.py +0 -0
  54. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_adapters/test_duckdb.py +0 -0
  55. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_bridge/__init__.py +0 -0
  56. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_bridge/test_compiler.py +0 -0
  57. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/__init__.py +0 -0
  58. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_contract.py +0 -0
  59. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_domain_model.py +0 -0
  60. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_load_semantic_source.py +0 -0
  61. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_prompt_renderers.py +0 -0
  62. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_scalability.py +0 -0
  63. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_schema.py +0 -0
  64. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_sdk_config.py +0 -0
  65. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_system_prompt_metrics.py +0 -0
  66. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_core/test_wildcard_tables.py +0 -0
  67. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_public_api.py +0 -0
  68. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_semantic/__init__.py +0 -0
  69. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_semantic/test_cube.py +0 -0
  70. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_semantic/test_dbt.py +0 -0
  71. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_semantic/test_relationships.py +0 -0
  72. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_semantic/test_search.py +0 -0
  73. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_semantic/test_yaml_source.py +0 -0
  74. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/__init__.py +0 -0
  75. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_auto_load.py +0 -0
  76. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_factory.py +0 -0
  77. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_middleware.py +0 -0
  78. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_pagination.py +0 -0
  79. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_relationship_tools.py +0 -0
  80. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_sdk.py +0 -0
  81. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_semantic_tools.py +0 -0
  82. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_tools/test_wildcard_tools.py +0 -0
  83. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_validation/__init__.py +0 -0
  84. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_validation/test_checkers.py +0 -0
  85. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_validation/test_explain.py +0 -0
  86. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_validation/test_relationship_checker.py +0 -0
  87. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_validation/test_result_checks.py +0 -0
  88. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_validation/test_sql_normalizer.py +0 -0
  89. {agentic_data_contracts-0.9.1 → agentic_data_contracts-0.9.2}/tests/test_validation/test_validator.py +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.9.2] - 2026-04-15
6
+
7
+ ### Fixed
8
+
9
+ - **Lazy session timer**: `ContractSession` no longer starts its wall-clock timer at construction. The timer now starts on the first `check_limits()` call, so idle time before the user's first interaction does not count against `temporal.max_duration_seconds`. This fixes premature "session expired" errors in long-lived agent setups (Chainlit, Webex bots) where the session object is created well before the first user message. (#16)
10
+
11
+ ### Added
12
+
13
+ - **`ContractSession.reset_timer()`**: New method that resets the duration timer so it restarts on the next `check_limits()` call. Useful for frameworks with their own idle-timeout mechanisms that want to restart the clock on user activity.
14
+
5
15
  ## [0.9.1] - 2026-04-13
6
16
 
7
17
  ### Added
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-data-contracts
3
- Version: 0.9.1
4
- Summary: YAML-first data contract governance for AI agents
3
+ Version: 0.9.2
4
+ Summary: YAML-first, domain-driven data governance for AI agents
5
5
  Project-URL: Homepage, https://github.com/flyersworder/agentic-data-contracts
6
6
  Project-URL: Repository, https://github.com/flyersworder/agentic-data-contracts
7
7
  Project-URL: Issues, https://github.com/flyersworder/agentic-data-contracts/issues
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://github.com/flyersworder/agentic-data-contrac
9
9
  Author-email: Qing <qingye779@gmail.com>
10
10
  License: MIT
11
11
  License-File: LICENSE
12
- Keywords: ai-agents,analytics,claude,data-contracts,data-governance,dbt,llm,sql-validation
12
+ Keywords: ai-agents,analytics,claude,data-contracts,data-governance,dbt,domain-driven,llm,semantic-layer,sql-validation
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: Science/Research
@@ -21,16 +21,16 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Requires-Python: >=3.12
23
23
  Requires-Dist: pydantic>=2.0
24
- Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: pyyaml>=6.0.3
25
25
  Requires-Dist: sqlglot>=23.0
26
26
  Requires-Dist: thefuzz>=0.22.1
27
27
  Provides-Extra: agent-contracts
28
- Requires-Dist: ai-agent-contracts>=0.2.0; extra == 'agent-contracts'
28
+ Requires-Dist: ai-agent-contracts>=0.3.0; extra == 'agent-contracts'
29
29
  Provides-Extra: agent-sdk
30
- Requires-Dist: claude-agent-sdk>=0.1.52; extra == 'agent-sdk'
30
+ Requires-Dist: claude-agent-sdk>=0.1.58; extra == 'agent-sdk'
31
31
  Provides-Extra: all
32
- Requires-Dist: ai-agent-contracts>=0.2.0; extra == 'all'
33
- Requires-Dist: claude-agent-sdk>=0.1.52; extra == 'all'
32
+ Requires-Dist: ai-agent-contracts>=0.3.0; extra == 'all'
33
+ Requires-Dist: claude-agent-sdk>=0.1.58; extra == 'all'
34
34
  Requires-Dist: duckdb; extra == 'all'
35
35
  Requires-Dist: google-cloud-bigquery; extra == 'all'
36
36
  Requires-Dist: psycopg2-binary; extra == 'all'
@@ -39,10 +39,10 @@ Provides-Extra: bigquery
39
39
  Requires-Dist: google-cloud-bigquery; extra == 'bigquery'
40
40
  Provides-Extra: dev
41
41
  Requires-Dist: duckdb; extra == 'dev'
42
- Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
43
- Requires-Dist: pytest-cov>=6.0; extra == 'dev'
44
- Requires-Dist: pytest>=8.0; extra == 'dev'
45
- Requires-Dist: ruff>=0.8.0; extra == 'dev'
42
+ Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
43
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
44
+ Requires-Dist: pytest>=9.0.3; extra == 'dev'
45
+ Requires-Dist: ruff>=0.15.10; extra == 'dev'
46
46
  Provides-Extra: duckdb
47
47
  Requires-Dist: duckdb; extra == 'duckdb'
48
48
  Provides-Extra: postgres
@@ -58,7 +58,7 @@ Description-Content-Type: text/markdown
58
58
  [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
59
59
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
60
60
 
61
- **Domain-driven data governance for AI agents.**
61
+ **YAML-first, domain-driven data governance for AI agents.**
62
62
 
63
63
  `agentic-data-contracts` takes a domain-driven approach to AI agent governance: instead of letting agents figure out your data landscape by trial and error, you teach them your business domains, metrics, and rules upfront — in YAML. The agent starts by understanding *what* a business domain means, then discovers *which* metrics to use, then builds queries that comply with your governance rules. All enforced automatically at query time via SQL validation powered by [sqlglot](https://github.com/tobymao/sqlglot).
64
64
 
@@ -68,6 +68,19 @@ Description-Content-Type: text/markdown
68
68
 
69
69
  ## How It Works
70
70
 
71
+ The agent follows a domain-driven workflow — understanding business context before writing SQL:
72
+
73
+ ```
74
+ 1. Agent receives: "How is revenue trending?"
75
+ 2. lookup_domain("revenue") → "Revenue is recognized at fulfillment, not booking"
76
+ 3. lookup_metric("total_revenue") → SUM(amount) FILTER (WHERE status = 'completed')
77
+ 4. Agent writes SQL using the metric definition
78
+ 5. validate_query(sql) → VALID (passes all contract rules)
79
+ 6. run_query(sql) → results returned
80
+ ```
81
+
82
+ Governance rules are enforced automatically at query time:
83
+
71
84
  ```
72
85
  Agent: "SELECT * FROM analytics.orders"
73
86
  -> BLOCKED (no SELECT * — specify explicit columns)
@@ -77,12 +90,9 @@ Agent: "SELECT order_id, amount FROM analytics.orders"
77
90
 
78
91
  Agent: "SELECT order_id, amount FROM analytics.orders WHERE tenant_id = 'acme'"
79
92
  -> PASSED + WARN (consider using semantic revenue definition)
80
-
81
- Agent: "DELETE FROM analytics.orders WHERE id = 1"
82
- -> BLOCKED (forbidden operation: DELETE)
83
93
  ```
84
94
 
85
- The contract defines the rules. The library enforces them — before the query ever reaches the database.
95
+ The contract defines the domains, metrics, and rules. The library enforces them — before the query ever reaches the database.
86
96
 
87
97
  ## Installation
88
98
 
@@ -123,6 +133,13 @@ semantic:
123
133
  - schema: marketing
124
134
  tables: [campaigns] # or list specific tables
125
135
  forbidden_operations: [DELETE, DROP, TRUNCATE, UPDATE, INSERT]
136
+ domains:
137
+ - name: revenue
138
+ summary: "Financial metrics from completed orders"
139
+ description: >
140
+ Revenue is recognized at fulfillment, not at booking.
141
+ Excludes refunds and chargebacks unless stated.
142
+ metrics: [total_revenue]
126
143
  rules:
127
144
  - name: tenant_isolation
128
145
  description: "All queries must filter by tenant_id"
@@ -216,7 +233,7 @@ asyncio.run(demo())
216
233
 
217
234
  | Tool | Description |
218
235
  |------|-------------|
219
- | `list_schemas` | List all allowed database schemas from the contract |
236
+ | `list_schemas` | List allowed schemas with descriptions and preferred flags |
220
237
  | `list_tables` | List allowed tables, optionally filtered by schema |
221
238
  | `describe_table` | Get full column details for an allowed table |
222
239
  | `preview_table` | Preview sample rows from an allowed table |
@@ -229,6 +246,57 @@ asyncio.run(demo())
229
246
  | `run_query` | Validate and execute a SQL query, returning results |
230
247
  | `get_contract_info` | Get the full contract: rules, limits, domains, and session status |
231
248
 
249
+ ## Domain-Driven Agent Workflow
250
+
251
+ The core design principle: **agents should understand the business domain before writing SQL.** Instead of dumping table schemas and hoping for the best, the contract teaches the agent your business vocabulary through progressive disclosure:
252
+
253
+ ```
254
+ 1. Domain context → "What does 'revenue' mean here?"
255
+ 2. Metric definitions → "How is 'total_revenue' calculated?"
256
+ 3. Query execution → "Run the validated SQL"
257
+ ```
258
+
259
+ ### Defining domains
260
+
261
+ Each domain carries a description that teaches the agent your business rules — things the SQL alone can't express:
262
+
263
+ ```yaml
264
+ semantic:
265
+ domains:
266
+ - name: acquisition
267
+ summary: "Customer acquisition costs and conversion metrics"
268
+ description: >
269
+ Acquisition metrics track the cost and efficiency of
270
+ acquiring new customers across all channels.
271
+ CAC is calculated using fully-loaded cost, not just ad spend.
272
+ metrics: [CAC, CPA, CPL, click_through_rate]
273
+ - name: retention
274
+ summary: "Customer retention, churn, and lifetime value"
275
+ description: >
276
+ Retention metrics measure how well we keep customers.
277
+ Churn is measured on a 30-day rolling window.
278
+ A customer is "active" if they had at least one qualifying
279
+ action in the window.
280
+ metrics: [churn_rate, LTV, retention_30d]
281
+ ```
282
+
283
+ ### How the agent uses domains
284
+
285
+ The system prompt gives the agent a compact domain index. When a user asks a domain-specific question, the agent explores progressively:
286
+
287
+ ```
288
+ lookup_domain("acquisition") → business context + metric descriptions
289
+ lookup_metric("CAC") → SQL expression, source table, filters
290
+ lookup_metric("acquisition cost") → fuzzy match, returns [CAC, CPA] as candidates
291
+ list_metrics(domain="retention") → all metrics in the retention domain
292
+ ```
293
+
294
+ This means the agent knows that "revenue is recognized at fulfillment, not at booking" *before* it writes a single line of SQL — reducing hallucinated metrics and incorrect calculations.
295
+
296
+ ### Why progressive disclosure works
297
+
298
+ This pattern — compact index in the prompt, detailed context on demand — is the same philosophy validated by agent skill systems, MCP tool servers, and RAG architectures. Instead of overloading the agent's context window with everything upfront, you give it just enough to know *where to look*, then let it pull details when needed. The result is better token efficiency, more focused reasoning, and fewer hallucinations from context overload.
299
+
232
300
  ## Contract Rules
233
301
 
234
302
  Rules are enforced at three levels:
@@ -397,53 +465,6 @@ dc = DataContract.from_yaml("contract.yml")
397
465
  print(dc.to_system_prompt(renderer=MarkdownRenderer()))
398
466
  ```
399
467
 
400
- ## Domain-Driven Agent Workflow
401
-
402
- The core design principle: **agents should understand the business domain before writing SQL.** Instead of dumping table schemas and hoping for the best, the contract teaches the agent your business vocabulary through progressive disclosure:
403
-
404
- ```
405
- 1. Domain context → "What does 'revenue' mean here?"
406
- 2. Metric definitions → "How is 'total_revenue' calculated?"
407
- 3. Query execution → "Run the validated SQL"
408
- ```
409
-
410
- ### Defining domains
411
-
412
- Each domain carries a description that teaches the agent your business rules — things the SQL alone can't express:
413
-
414
- ```yaml
415
- semantic:
416
- domains:
417
- - name: acquisition
418
- summary: "Customer acquisition costs and conversion metrics"
419
- description: >
420
- Acquisition metrics track the cost and efficiency of
421
- acquiring new customers across all channels.
422
- CAC is calculated using fully-loaded cost, not just ad spend.
423
- metrics: [CAC, CPA, CPL, click_through_rate]
424
- - name: retention
425
- summary: "Customer retention, churn, and lifetime value"
426
- description: >
427
- Retention metrics measure how well we keep customers.
428
- Churn is measured on a 30-day rolling window.
429
- A customer is "active" if they had at least one qualifying
430
- action in the window.
431
- metrics: [churn_rate, LTV, retention_30d]
432
- ```
433
-
434
- ### How the agent uses domains
435
-
436
- The system prompt gives the agent a compact domain index. When a user asks a domain-specific question, the agent explores progressively:
437
-
438
- ```
439
- lookup_domain("acquisition") → business context + metric descriptions
440
- lookup_metric("CAC") → SQL expression, source table, filters
441
- lookup_metric("acquisition cost") → fuzzy match, returns [CAC, CPA] as candidates
442
- list_metrics(domain="retention") → all metrics in the retention domain
443
- ```
444
-
445
- This means the agent knows that "revenue is recognized at fulfillment, not at booking" *before* it writes a single line of SQL — reducing hallucinated metrics and incorrect calculations.
446
-
447
468
  ## Scaling to Large Organizations
448
469
 
449
470
  Tested for 200+ tables, 300+ metrics, 50+ relationships across multiple schemas.
@@ -5,7 +5,7 @@
5
5
  [![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- **Domain-driven data governance for AI agents.**
8
+ **YAML-first, domain-driven data governance for AI agents.**
9
9
 
10
10
  `agentic-data-contracts` takes a domain-driven approach to AI agent governance: instead of letting agents figure out your data landscape by trial and error, you teach them your business domains, metrics, and rules upfront — in YAML. The agent starts by understanding *what* a business domain means, then discovers *which* metrics to use, then builds queries that comply with your governance rules. All enforced automatically at query time via SQL validation powered by [sqlglot](https://github.com/tobymao/sqlglot).
11
11
 
@@ -15,6 +15,19 @@
15
15
 
16
16
  ## How It Works
17
17
 
18
+ The agent follows a domain-driven workflow — understanding business context before writing SQL:
19
+
20
+ ```
21
+ 1. Agent receives: "How is revenue trending?"
22
+ 2. lookup_domain("revenue") → "Revenue is recognized at fulfillment, not booking"
23
+ 3. lookup_metric("total_revenue") → SUM(amount) FILTER (WHERE status = 'completed')
24
+ 4. Agent writes SQL using the metric definition
25
+ 5. validate_query(sql) → VALID (passes all contract rules)
26
+ 6. run_query(sql) → results returned
27
+ ```
28
+
29
+ Governance rules are enforced automatically at query time:
30
+
18
31
  ```
19
32
  Agent: "SELECT * FROM analytics.orders"
20
33
  -> BLOCKED (no SELECT * — specify explicit columns)
@@ -24,12 +37,9 @@ Agent: "SELECT order_id, amount FROM analytics.orders"
24
37
 
25
38
  Agent: "SELECT order_id, amount FROM analytics.orders WHERE tenant_id = 'acme'"
26
39
  -> PASSED + WARN (consider using semantic revenue definition)
27
-
28
- Agent: "DELETE FROM analytics.orders WHERE id = 1"
29
- -> BLOCKED (forbidden operation: DELETE)
30
40
  ```
31
41
 
32
- The contract defines the rules. The library enforces them — before the query ever reaches the database.
42
+ The contract defines the domains, metrics, and rules. The library enforces them — before the query ever reaches the database.
33
43
 
34
44
  ## Installation
35
45
 
@@ -70,6 +80,13 @@ semantic:
70
80
  - schema: marketing
71
81
  tables: [campaigns] # or list specific tables
72
82
  forbidden_operations: [DELETE, DROP, TRUNCATE, UPDATE, INSERT]
83
+ domains:
84
+ - name: revenue
85
+ summary: "Financial metrics from completed orders"
86
+ description: >
87
+ Revenue is recognized at fulfillment, not at booking.
88
+ Excludes refunds and chargebacks unless stated.
89
+ metrics: [total_revenue]
73
90
  rules:
74
91
  - name: tenant_isolation
75
92
  description: "All queries must filter by tenant_id"
@@ -163,7 +180,7 @@ asyncio.run(demo())
163
180
 
164
181
  | Tool | Description |
165
182
  |------|-------------|
166
- | `list_schemas` | List all allowed database schemas from the contract |
183
+ | `list_schemas` | List allowed schemas with descriptions and preferred flags |
167
184
  | `list_tables` | List allowed tables, optionally filtered by schema |
168
185
  | `describe_table` | Get full column details for an allowed table |
169
186
  | `preview_table` | Preview sample rows from an allowed table |
@@ -176,6 +193,57 @@ asyncio.run(demo())
176
193
  | `run_query` | Validate and execute a SQL query, returning results |
177
194
  | `get_contract_info` | Get the full contract: rules, limits, domains, and session status |
178
195
 
196
+ ## Domain-Driven Agent Workflow
197
+
198
+ The core design principle: **agents should understand the business domain before writing SQL.** Instead of dumping table schemas and hoping for the best, the contract teaches the agent your business vocabulary through progressive disclosure:
199
+
200
+ ```
201
+ 1. Domain context → "What does 'revenue' mean here?"
202
+ 2. Metric definitions → "How is 'total_revenue' calculated?"
203
+ 3. Query execution → "Run the validated SQL"
204
+ ```
205
+
206
+ ### Defining domains
207
+
208
+ Each domain carries a description that teaches the agent your business rules — things the SQL alone can't express:
209
+
210
+ ```yaml
211
+ semantic:
212
+ domains:
213
+ - name: acquisition
214
+ summary: "Customer acquisition costs and conversion metrics"
215
+ description: >
216
+ Acquisition metrics track the cost and efficiency of
217
+ acquiring new customers across all channels.
218
+ CAC is calculated using fully-loaded cost, not just ad spend.
219
+ metrics: [CAC, CPA, CPL, click_through_rate]
220
+ - name: retention
221
+ summary: "Customer retention, churn, and lifetime value"
222
+ description: >
223
+ Retention metrics measure how well we keep customers.
224
+ Churn is measured on a 30-day rolling window.
225
+ A customer is "active" if they had at least one qualifying
226
+ action in the window.
227
+ metrics: [churn_rate, LTV, retention_30d]
228
+ ```
229
+
230
+ ### How the agent uses domains
231
+
232
+ The system prompt gives the agent a compact domain index. When a user asks a domain-specific question, the agent explores progressively:
233
+
234
+ ```
235
+ lookup_domain("acquisition") → business context + metric descriptions
236
+ lookup_metric("CAC") → SQL expression, source table, filters
237
+ lookup_metric("acquisition cost") → fuzzy match, returns [CAC, CPA] as candidates
238
+ list_metrics(domain="retention") → all metrics in the retention domain
239
+ ```
240
+
241
+ This means the agent knows that "revenue is recognized at fulfillment, not at booking" *before* it writes a single line of SQL — reducing hallucinated metrics and incorrect calculations.
242
+
243
+ ### Why progressive disclosure works
244
+
245
+ This pattern — compact index in the prompt, detailed context on demand — is the same philosophy validated by agent skill systems, MCP tool servers, and RAG architectures. Instead of overloading the agent's context window with everything upfront, you give it just enough to know *where to look*, then let it pull details when needed. The result is better token efficiency, more focused reasoning, and fewer hallucinations from context overload.
246
+
179
247
  ## Contract Rules
180
248
 
181
249
  Rules are enforced at three levels:
@@ -344,53 +412,6 @@ dc = DataContract.from_yaml("contract.yml")
344
412
  print(dc.to_system_prompt(renderer=MarkdownRenderer()))
345
413
  ```
346
414
 
347
- ## Domain-Driven Agent Workflow
348
-
349
- The core design principle: **agents should understand the business domain before writing SQL.** Instead of dumping table schemas and hoping for the best, the contract teaches the agent your business vocabulary through progressive disclosure:
350
-
351
- ```
352
- 1. Domain context → "What does 'revenue' mean here?"
353
- 2. Metric definitions → "How is 'total_revenue' calculated?"
354
- 3. Query execution → "Run the validated SQL"
355
- ```
356
-
357
- ### Defining domains
358
-
359
- Each domain carries a description that teaches the agent your business rules — things the SQL alone can't express:
360
-
361
- ```yaml
362
- semantic:
363
- domains:
364
- - name: acquisition
365
- summary: "Customer acquisition costs and conversion metrics"
366
- description: >
367
- Acquisition metrics track the cost and efficiency of
368
- acquiring new customers across all channels.
369
- CAC is calculated using fully-loaded cost, not just ad spend.
370
- metrics: [CAC, CPA, CPL, click_through_rate]
371
- - name: retention
372
- summary: "Customer retention, churn, and lifetime value"
373
- description: >
374
- Retention metrics measure how well we keep customers.
375
- Churn is measured on a 30-day rolling window.
376
- A customer is "active" if they had at least one qualifying
377
- action in the window.
378
- metrics: [churn_rate, LTV, retention_30d]
379
- ```
380
-
381
- ### How the agent uses domains
382
-
383
- The system prompt gives the agent a compact domain index. When a user asks a domain-specific question, the agent explores progressively:
384
-
385
- ```
386
- lookup_domain("acquisition") → business context + metric descriptions
387
- lookup_metric("CAC") → SQL expression, source table, filters
388
- lookup_metric("acquisition cost") → fuzzy match, returns [CAC, CPA] as candidates
389
- list_metrics(domain="retention") → all metrics in the retention domain
390
- ```
391
-
392
- This means the agent knows that "revenue is recognized at fulfillment, not at booking" *before* it writes a single line of SQL — reducing hallucinated metrics and incorrect calculations.
393
-
394
415
  ## Scaling to Large Organizations
395
416
 
396
417
  Tested for 200+ tables, 300+ metrics, 50+ relationships across multiple schemas.
@@ -1,7 +1,7 @@
1
1
  # Agentic Data Contracts — Architecture
2
2
 
3
3
  **Date:** 2026-04-13
4
- **Status:** Implemented (v0.9.1)
4
+ **Status:** Implemented (v0.9.2)
5
5
  **Author:** Qing Ye + Claude
6
6
 
7
7
  ## Problem Statement
@@ -188,7 +188,7 @@ When `ai-agent-contracts` is NOT installed, `ContractSession` provides self-cont
188
188
 
189
189
  - **Retry count** — incremented on each failed query attempt, checked against `max_retries`
190
190
  - **Token usage** — tracked via callback, checked against `token_budget`
191
- - **Wall-clock duration** — start time recorded, checked against `max_duration_seconds`
191
+ - **Wall-clock duration** — lazy start on first `check_limits()` call (not at construction), checked against `max_duration_seconds`. Can be reset via `reset_timer()` for frameworks that manage their own idle timeouts.
192
192
  - **Cost estimate** — if EXPLAIN adapter returns cost info, checked against `cost_limit_usd`
193
193
 
194
194
  These are simple counters/timers with guard checks before each tool call. No formal state machine.
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "agentic-data-contracts"
3
- version = "0.9.1"
4
- description = "YAML-first data contract governance for AI agents"
3
+ version = "0.9.2"
4
+ description = "YAML-first, domain-driven data governance for AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  authors = [{ name = "Qing", email = "qingye779@gmail.com" }]
@@ -10,9 +10,11 @@ keywords = [
10
10
  "ai-agents",
11
11
  "data-contracts",
12
12
  "data-governance",
13
+ "domain-driven",
13
14
  "sql-validation",
14
15
  "llm",
15
16
  "claude",
17
+ "semantic-layer",
16
18
  "analytics",
17
19
  "dbt",
18
20
  ]
@@ -30,7 +32,7 @@ classifiers = [
30
32
  dependencies = [
31
33
  "sqlglot>=23.0",
32
34
  "pydantic>=2.0",
33
- "pyyaml>=6.0",
35
+ "pyyaml>=6.0.3",
34
36
  "thefuzz>=0.22.1",
35
37
  ]
36
38
 
@@ -41,17 +43,17 @@ Issues = "https://github.com/flyersworder/agentic-data-contracts/issues"
41
43
  Documentation = "https://github.com/flyersworder/agentic-data-contracts/blob/main/docs/architecture.md"
42
44
 
43
45
  [project.optional-dependencies]
44
- agent-sdk = ["claude-agent-sdk>=0.1.52"]
45
- agent-contracts = ["ai-agent-contracts>=0.2.0"]
46
+ agent-sdk = ["claude-agent-sdk>=0.1.58"]
47
+ agent-contracts = ["ai-agent-contracts>=0.3.0"]
46
48
  bigquery = ["google-cloud-bigquery"]
47
49
  snowflake = ["snowflake-connector-python"]
48
50
  postgres = ["psycopg2-binary"]
49
51
  duckdb = ["duckdb"]
50
52
  dev = [
51
- "pytest>=8.0",
52
- "pytest-asyncio>=0.23",
53
- "pytest-cov>=6.0",
54
- "ruff>=0.8.0",
53
+ "pytest>=9.0.3",
54
+ "pytest-asyncio>=1.3.0",
55
+ "pytest-cov>=7.1.0",
56
+ "ruff>=0.15.10",
55
57
  "duckdb",
56
58
  ]
57
59
  all = [
@@ -20,12 +20,23 @@ class ContractSession:
20
20
  self.retries: int = 0
21
21
  self.tokens_used: int = 0
22
22
  self.cost_usd: float = 0.0
23
- self._start_time: float = time.monotonic()
23
+ self._start_time: float | None = None
24
+
25
+ def _ensure_timer(self) -> None:
26
+ """Start the timer if not already running."""
27
+ if self._start_time is None:
28
+ self._start_time = time.monotonic()
24
29
 
25
30
  @property
26
31
  def elapsed_seconds(self) -> float:
32
+ if self._start_time is None:
33
+ return 0.0
27
34
  return time.monotonic() - self._start_time
28
35
 
36
+ def reset_timer(self) -> None:
37
+ """Reset the timer so it restarts on the next check_limits() call."""
38
+ self._start_time = None
39
+
29
40
  def record_retry(self) -> None:
30
41
  self.retries += 1
31
42
 
@@ -36,6 +47,7 @@ class ContractSession:
36
47
  self.cost_usd += amount
37
48
 
38
49
  def check_limits(self) -> None:
50
+ self._ensure_timer()
39
51
  res = self.contract.schema.resources
40
52
  if res is None:
41
53
  return
@@ -60,6 +60,7 @@ def test_session_blocks_on_cost_limit(fixtures_dir: Path) -> None:
60
60
  def test_session_elapsed_seconds(fixtures_dir: Path) -> None:
61
61
  dc = DataContract.from_yaml(fixtures_dir / "valid_contract.yml")
62
62
  session = ContractSession(dc)
63
+ session.check_limits() # starts the timer
63
64
  assert session.elapsed_seconds >= 0.0
64
65
 
65
66
 
@@ -85,3 +86,61 @@ def test_session_remaining_budget(fixtures_dir: Path) -> None:
85
86
  assert info["retries_remaining"] == 2
86
87
  assert info["tokens_remaining"] == 40000
87
88
  assert info["cost_remaining_usd"] == pytest.approx(3.50)
89
+
90
+
91
+ # --- Lazy timer tests (issue #16) ---
92
+
93
+
94
+ def test_timer_not_started_at_construction(fixtures_dir: Path) -> None:
95
+ """Timer should not start at construction — elapsed should be 0."""
96
+ dc = DataContract.from_yaml(fixtures_dir / "valid_contract.yml")
97
+ session = ContractSession(dc)
98
+ assert session.elapsed_seconds == 0.0
99
+
100
+
101
+ def test_timer_starts_on_first_check_limits(fixtures_dir: Path) -> None:
102
+ """Timer should start lazily on the first check_limits() call."""
103
+ dc = DataContract.from_yaml(fixtures_dir / "valid_contract.yml")
104
+ session = ContractSession(dc)
105
+ assert session._start_time is None
106
+ session.check_limits()
107
+ assert session._start_time is not None
108
+ assert session.elapsed_seconds >= 0.0
109
+
110
+
111
+ def test_reset_timer(fixtures_dir: Path) -> None:
112
+ """reset_timer() should clear the timer so elapsed returns 0."""
113
+ dc = DataContract.from_yaml(fixtures_dir / "valid_contract.yml")
114
+ session = ContractSession(dc)
115
+ session.check_limits() # starts timer
116
+ session.reset_timer()
117
+ assert session.elapsed_seconds == 0.0
118
+ assert session._start_time is None
119
+
120
+
121
+ def test_reset_timer_restarts_on_next_check(fixtures_dir: Path) -> None:
122
+ """After reset, timer should restart on the next check_limits() call."""
123
+ dc = DataContract.from_yaml(fixtures_dir / "valid_contract.yml")
124
+ session = ContractSession(dc)
125
+ session.check_limits() # starts timer
126
+ session.reset_timer()
127
+ session.check_limits() # restarts timer
128
+ assert session._start_time is not None
129
+
130
+
131
+ def test_reset_timer_before_started_is_noop(fixtures_dir: Path) -> None:
132
+ """reset_timer() on an unstarted timer should be a safe no-op."""
133
+ dc = DataContract.from_yaml(fixtures_dir / "valid_contract.yml")
134
+ session = ContractSession(dc)
135
+ session.reset_timer() # should not raise
136
+ assert session._start_time is None
137
+ assert session.elapsed_seconds == 0.0
138
+
139
+
140
+ def test_remaining_before_timer_started(fixtures_dir: Path) -> None:
141
+ """remaining() should show full duration budget when timer not started."""
142
+ dc = DataContract.from_yaml(fixtures_dir / "valid_contract.yml")
143
+ session = ContractSession(dc)
144
+ info = session.remaining()
145
+ assert "seconds_remaining" in info
146
+ assert info["seconds_remaining"] == 300.0
@@ -8,12 +8,12 @@ resolution-markers = [
8
8
  ]
9
9
 
10
10
  [options]
11
- exclude-newer = "2026-04-10T18:45:17.227262Z"
11
+ exclude-newer = "2026-04-12T16:17:12.933746Z"
12
12
  exclude-newer-span = "P3D"
13
13
 
14
14
  [[package]]
15
15
  name = "agentic-data-contracts"
16
- version = "0.9.1"
16
+ version = "0.9.2"
17
17
  source = { editable = "." }
18
18
  dependencies = [
19
19
  { name = "pydantic" },
@@ -60,18 +60,18 @@ snowflake = [
60
60
  [package.metadata]
61
61
  requires-dist = [
62
62
  { name = "agentic-data-contracts", extras = ["agent-sdk", "agent-contracts", "bigquery", "snowflake", "postgres", "duckdb"], marker = "extra == 'all'" },
63
- { name = "ai-agent-contracts", marker = "extra == 'agent-contracts'", specifier = ">=0.2.0" },
64
- { name = "claude-agent-sdk", marker = "extra == 'agent-sdk'", specifier = ">=0.1.52" },
63
+ { name = "ai-agent-contracts", marker = "extra == 'agent-contracts'", specifier = ">=0.3.0" },
64
+ { name = "claude-agent-sdk", marker = "extra == 'agent-sdk'", specifier = ">=0.1.58" },
65
65
  { name = "duckdb", marker = "extra == 'dev'" },
66
66
  { name = "duckdb", marker = "extra == 'duckdb'" },
67
67
  { name = "google-cloud-bigquery", marker = "extra == 'bigquery'" },
68
68
  { name = "psycopg2-binary", marker = "extra == 'postgres'" },
69
69
  { name = "pydantic", specifier = ">=2.0" },
70
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
71
- { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" },
72
- { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0" },
73
- { name = "pyyaml", specifier = ">=6.0" },
74
- { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" },
70
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
71
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.3.0" },
72
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" },
73
+ { name = "pyyaml", specifier = ">=6.0.3" },
74
+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.10" },
75
75
  { name = "snowflake-connector-python", marker = "extra == 'snowflake'" },
76
76
  { name = "sqlglot", specifier = ">=23.0" },
77
77
  { name = "thefuzz", specifier = ">=0.22.1" },
@@ -2202,27 +2202,27 @@ wheels = [
2202
2202
 
2203
2203
  [[package]]
2204
2204
  name = "ruff"
2205
- version = "0.15.9"
2206
- source = { registry = "https://pypi.org/simple" }
2207
- sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" }
2208
- wheels = [
2209
- { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" },
2210
- { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" },
2211
- { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" },
2212
- { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" },
2213
- { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" },
2214
- { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" },
2215
- { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" },
2216
- { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" },
2217
- { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" },
2218
- { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" },
2219
- { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" },
2220
- { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" },
2221
- { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" },
2222
- { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" },
2223
- { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" },
2224
- { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" },
2225
- { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" },
2205
+ version = "0.15.10"
2206
+ source = { registry = "https://pypi.org/simple" }
2207
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
2208
+ wheels = [
2209
+ { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
2210
+ { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
2211
+ { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
2212
+ { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
2213
+ { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
2214
+ { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
2215
+ { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
2216
+ { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
2217
+ { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
2218
+ { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
2219
+ { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
2220
+ { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
2221
+ { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
2222
+ { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
2223
+ { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
2224
+ { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
2225
+ { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
2226
2226
  ]
2227
2227
 
2228
2228
  [[package]]