diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000000..93a965733d --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,3 @@ +# Principles + +Always prioritize KISS principle and code readibility. diff --git a/.claude/agents/ea_code_reviewer.md b/.claude/agents/ea_code_reviewer.md new file mode 100644 index 0000000000..4c2e89076f --- /dev/null +++ b/.claude/agents/ea_code_reviewer.md @@ -0,0 +1,168 @@ +# EA Code Reviewer + +## Goal + +Review the generated EA code for code quality, correctness, and proper framework usage. You are a strict code reviewer ensuring the EA is production-ready. + +## Review Checklist + +### 1. Framework Component Selection + +**Principle:** Use the most specific framework component available for the use case. + +Check: + +- [ ] Transport type matches data source (REST → `HttpTransport`, WebSocket → `WebSocketTransport`, SSE → `SseTransport`) +- [ ] Endpoint type matches data type (price feeds → `PriceEndpoint`, bid/ask → `LwbaEndpoint`, stocks → `StockEndpoint`, PoR → PoR endpoints) +- [ ] Adapter type matches endpoint types (`PriceAdapter` for price endpoints, `PoRAdapter` for PoR) +- [ ] Using `EmptyInputParameters` when no input params needed (not `new InputParameters({}, [{}])`) +- [ ] Using framework response types (`SingleNumberResultResponse`, etc.) +- [ ] Configuration uses `AdapterConfig` with proper types + +**Reference:** Check `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/` for available components. + +### 2. Code Quality Principles + +#### DRY (Don't Repeat Yourself) + +- [ ] No duplicated logic across files +- [ ] Shared constants/types extracted appropriately +- [ ] No copy-paste code blocks + +#### KISS (Keep It Simple) + +- [ ] No unnecessary abstractions or wrapper functions +- [ ] No over-engineering for hypothetical future needs +- [ ] Direct, readable implementations preferred +- [ ] Avoid shallow wrappers around native/framework functions + +#### Single Responsibility + +- [ ] Each file/function has one clear purpose +- [ ] Transport handles data fetching, endpoint handles routing, config handles settings + +#### Readability + +- [ ] Clear, descriptive variable and function names +- [ ] Consistent code style +- [ ] Logical code organization +- [ ] No overly complex one-liners + +### 3. Correctness + +#### Type Safety + +- [ ] Proper TypeScript types throughout +- [ ] No `any` types without justification +- [ ] Input/output types match framework expectations +- [ ] Generic types properly constrained + +#### Precision & Numbers + +- [ ] Large integers use `BigInt`, `bignumber.js`, or `ethers.toBigInt()` +- [ ] Floating point math uses `decimal.js` with adequate precision +- [ ] No precision loss from `parseInt`/`parseFloat` on large numbers +- [ ] Maintain 18 decimal precision for on-chain feeds + +#### Error Handling + +- [ ] Appropriate use of framework error types (`AdapterError`, `AdapterInputError`, etc.) +- [ ] Clear, actionable error messages +- [ ] No swallowed errors without logging +- [ ] Graceful handling of provider failures + +### 4. Performance & Efficiency + +#### Request Optimization + +- [ ] `prepareRequests` batches params efficiently (not N identical requests for N params) +- [ ] Minimal API calls to achieve the result +- [ ] No redundant data fetching + +#### Resource Usage + +- [ ] No memory leaks (cleanup in transports if needed) +- [ ] Efficient data parsing (no unnecessary transformations) + +### 5. Configuration + +- [ ] All external URLs/secrets from config, not hardcoded +- [ ] Required vs optional config properly specified +- [ ] Sensible defaults where appropriate +- [ ] Config types match expected values (string, number, boolean, enum) + +### 6. Specialized Library Usage + +Use appropriate libraries instead of raw HTTP: + +| Use Case | Library | Instead Of | +| ------------------ | ------------------------- | ----------------- | +| EVM contract calls | `ethers` | Raw JSON-RPC HTTP | +| Solana programs | `@solana/web3.js` | Raw HTTP | +| Large integers | `bignumber.js` / `BigInt` | `Number` | +| Decimal math | `decimal.js` | Native floats | + +### 7. Transport Implementation + +For `HttpTransport`: + +- [ ] `prepareRequests` returns minimal request set +- [ ] `parseResponse` correctly maps responses to params +- [ ] Error responses handled properly +- [ ] Response types match endpoint expectations + +For `WebSocketTransport`: + +- [ ] `url` configuration correct +- [ ] `handlers.message` parses messages correctly +- [ ] Subscribe/unsubscribe builders if needed + +For custom transports: + +- [ ] Properly extends `SubscriptionTransport` or appropriate base +- [ ] Background execution handled correctly + +### 8. Input Validation + +- [ ] Input parameters properly defined with types +- [ ] Required vs optional clearly specified +- [ ] Aliases defined where helpful +- [ ] Clear descriptions for each parameter + +## Review Process + +1. **Read all source files** in the EA package (`src/`, `config/`, `transport/`, `endpoint/`) +2. **Evaluate against each checklist item** +3. **Consider the requirements** - some patterns may be justified by specific needs +4. **Assess overall quality** - is this production-ready code? + +## Output Format + +Return structured output: + +- `approved`: boolean - whether code passes review (true = production-ready) +- `rationale`: string - if not approved, list all issues with file paths and specific fixes needed. Format each issue as: `[file:line] category: description. Fix: suggestion` + +## Severity Guidelines + +**Must Fix (blocks approval):** + +- Wrong framework component for use case +- Precision loss on numeric values +- Missing error handling for critical paths +- Hardcoded secrets/URLs +- Inefficient request patterns (N requests when 1 would do) + +**Should Fix (recommend but may approve with note):** + +- Minor code style issues +- Verbose but correct implementations +- Missing optional optimizations + +**Context-Dependent:** + +- Some EAs need custom transports +- Some use cases genuinely need raw HTTP +- Requirements may justify unusual patterns + +When in doubt, flag for human review but explain the context. diff --git a/.claude/agents/ea_developer.md b/.claude/agents/ea_developer.md new file mode 100644 index 0000000000..8911d41130 --- /dev/null +++ b/.claude/agents/ea_developer.md @@ -0,0 +1,354 @@ +# Chainlink External Adapters (EAs) Development Guide + +## 1.GOAL + +Create an EA according to the specifications in the requirements YAML provided by the user by following this developement guide step by step using the chainlink-external-adapter-framework at @.yarn/unplugged/@chainlink-external-adapter-framework-npm-\* + +CRITICAL: FOLLOWING REQUIREMENTS ARE REQUIRED AND MUST FOLLOW + +- ALL SPECIFIACTIONS IN THE YAML REQUIREMENT MUST BE STRICTLY IMPLEMENTED AS REQUESTED +- Prioritize readable, maintainable tests over comprehensive coverage +- Use the best suited components from EA framework @.yarn/unplugged/@chainlink-external-adapter-framework-npm-\* + +## 2. TODO LIST + +- [] Analyze and read provided requirement YAML and this development guide +- [] Setup the EA Folder Structure with Required Commands +- [] Create Transports +- [] Create Endpoints +- [] Create Adapters +- [] Setup Config +- [] Additional Necessary Items ... + +## 3. Example EA Request and Response format + +Follow the requested schemas and format of EA request and response provided in the yaml requirements. + +### Example Request Format (EA Invocation) + +```json +{ + "data": { + "endpoint": "nav", + "ticker": "WTGXX", + "transport": "rest" + } +} +``` + +### Example Response Format (EA Response) + +```json +{ + "data": { + "result": 1 + }, + "result": 1, + "statusCode": 200, + "timestamps": { + "providerDataRequestedUnixMs": 978347471111, + "providerDataReceivedUnixMs": 978347471111, + "providerIndicatedTimeUnixMs": 1750204800000 + } +} +``` + +--- + +## 4. Development Walkthrough - New Source EA + +### Prerequisites + +- Node.js 22 +- Yarn + +### Step 1: Setup the EA Folder Structure with Required Commands + +CRITICAL:MUST USE FOLLOWING REQUIRED COMMANDs + +#### 1.create the package + +```bash +yarn new source +``` + +#### 2.execute the yo command generated by the previous step + +use + +```bash +EXTERNAL_ADAPTER_GENERATOR_NO_INTERACTIVE=true [yo command] +``` + +ignore `yarn new tsconfig` for now because we need to rename the example-adapter first + +#### 3.rename the folder from `example-adapter` to requested adpater name + +```bash +mv packages/sources/example-adapter packages/sources/[requested-adapter-name] +``` + +#### 4. execute `yarn new tsconfig` + +--- + +### Step 2: Create Transports + +Define a transport in the `transport/` folder. **Read the framework source for implementation details:** + +| Transport Type | Framework Source | Use Case | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | +| `HttpTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/http.d.ts` | HTTP REST API requests | +| `WebSocketTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/websocket.d.ts` | Real-time WebSocket data | +| `SseTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/sse.d.ts` | Server-Sent Events | +| Abstract transports | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/` | Base classes for custom transports | + +**Key concepts:** + +- `HttpTransport` requires `prepareRequests` and `parseResponse` methods +- `WebSocketTransport` requires `url`, `handlers.message`, and optionally `builders` for subscribe/unsubscribe +- Read the `.d.ts` files for exact type definitions and required config + +**Real-world examples:** `packages/sources/coingecko/src/transport/`, `packages/sources/coinpaprika/src/transport/` + +--- + +### Step 3: Create Endpoints + +Define endpoints in the `endpoint/` folder. **Read the framework source for endpoint types:** + +| Endpoint Type | Framework Source | Use Case | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| `AdapterEndpoint` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/endpoint.d.ts` | Generic endpoints | +| `PriceEndpoint`, `CryptoPriceEndpoint`, `ForexPriceEndpoint` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/price.d.ts` | Price feeds with base/quote params | +| `LwbaEndpoint` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/lwba.d.ts` | Lightweight bid/ask (bid, mid, ask) | +| PoR endpoints | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/por.d.ts` | Proof of Reserves | +| `MarketStatusEndpoint` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/market-status.d.ts` | Market open/closed status | +| `StockEndpoint` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/stock.d.ts` | Stock data feeds | + +**Input Parameters:** + +| Type | Framework Source | Use Case | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| `InputParameters` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/validation/input-params.d.ts` | Custom input params | +| `EmptyInputParameters` | Same file as above | No input params needed | +| `priceEndpointInputParametersDefinition` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/price.d.ts` | Predefined base/quote params | +| `lwbaEndpointInputParametersDefinition` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/lwba.d.ts` | Predefined LWBA params | + +**Real-world examples:** `packages/sources/coingecko/src/endpoint/`, `packages/sources/wbtc-address-set/src/endpoint/` + +--- + +### Step 4: Create Adapters + +Create the adapter in `src/index.ts`. **Read the framework source:** + +| Adapter Type | Framework Source | Use Case | +| -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| `Adapter` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/basic.d.ts` | Generic adapter | +| `PriceAdapter` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/price.d.ts` | Price feeds with includes support | +| `PoRAdapter` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/por.d.ts` | Proof of Reserves | +| `expose` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/index.d.ts` | Server startup | + +**Real-world examples:** `packages/sources/coingecko/src/index.ts`, `packages/sources/wisdomtree/src/index.ts` + +--- + +### Step 5: Setup Config + +Define configuration in `config/index.ts`. **Read the framework source:** + +| Component | Framework Source | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `AdapterConfig` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/config/index.d.ts` | +| Base settings | Same file - see `BaseSettingsDefinition` for all available base settings | + +**Config types supported:** `string`, `number`, `boolean`, `enum` + +**Real-world examples:** `packages/sources/coingecko/src/config.ts`, `packages/sources/wisdomtree/src/config.ts` + +--- + +### Development Tips + +- For every implementation, look up if there are similar implementaitons exist in the other deployed EA packages +- Keep **input validation** strict; provide clear error messages +- Prefer **typed responses** (e.g., `SingleNumberResultResponse`) and avoid leaking provider schema +- Include **test-payload.json** for PR soak checks + +--- + +## 6. Best Practices & Common Pitfalls + +**Keep It Simple, Stupid** + +- Apply KISS principle to code implementation +- Avoid shallow, wrappers of language/framework native functions + +**Input Validation** + +- Validate inputs rigorously +- Provide helpful, clear error messages + +**Output Normalization** + +- Normalize outputs consistently +- Avoid provider-specific field leakage +- Prefer typed responses (e.g., `SingleNumberResultResponse`) + +**Rate Limiting & Caching** + +- Configure appropriate rate limits +- Document limits in README + +**Documentation** + +- Keep README minimal +- Rely on auto-generated docs + +**Reliability** + +- Prefer idempotent operations +- Be mindful of retries +- maintain precision tolerance of 18 decimals for on-chain feed + +**Large Integer & Precision Handling** + +When dealing with large values (e.g., blockchain uint256, token amounts, hex-encoded RPC responses): + +- **NEVER use JavaScript `number` for values that may exceed `Number.MAX_SAFE_INTEGER` (2^53 - 1)** +- Converting `BigInt` to `Number` silently loses precision without throwing errors +- The framework's `Result` type accepts `string | number | null` - use `string` for large values + +| Scenario | Approach | Example | +| -------------------------- | ------------------------------------ | ----------------------------- | +| Hex to large integer | Return as decimal string | `BigInt(hexValue).toString()` | +| Token amounts (uint256) | Use `bignumber.js` or keep as string | `new BigNumber(value)` | +| Known small values | `number` is acceptable | Timestamps, small counts | +| Arithmetic on large values | Use `bignumber.js` | Avoid native JS operators | + +**Patterns from existing EAs:** + +- `packages/composites/proof-of-reserves/` - returns `bigint`, converts to string for response +- `packages/sources/view-starknet-latest-answer/` - uses `num.hexToDecimalString()` from starknet.js +- `packages/sources/xusd-usd-exchange-rate/` - `hexToDecimalString()` for full precision + +```typescript +// ✅ CORRECT: Preserves precision for arbitrarily large values +export function hexToDecimalString(resultHex: string): string { + return BigInt(resultHex).toString() +} + +// ❌ WRONG: Silent precision loss for values > MAX_SAFE_INTEGER +export function hexToNumber(resultHex: string): number { + return Number(BigInt(resultHex)) // 9007199254740993n → 9007199254740992 +} +``` + +**Naming convention** +clear, readable best practice names + +**Common Pattern** +Use https://github.com/MikeMcl/bignumber.js/ when operating on large integers +Use https://github.com/MikeMcl/decimal.js/ for all floating point operations. Decimal.js uses a precision of 20 by default, but we may lose some precision with really large numbers, so please update to a higher precision before usage + +**Native JS Packages** + +Use native JS packages when the framework doesn't provide the needed functionality: + +| Use Case | Package | When to Use | +| --------------------- | --------------------------- | ---------------------------------------------------------------------- | +| EVM on-chain reads | `ethers` | Reading smart contracts, calling view functions, encoding/decoding ABI | +| Solana on-chain reads | `@solana/web3.js` | Reading Solana accounts, programs | +| Large integers | `bignumber.js` | Handling uint256, token amounts | +| Decimal math | `decimal.js` | Precise floating point operations | +| Custom HTTP | `axios` | When HttpTransport doesn't fit (rare) | +| Cryptography | `crypto` (Node.js built-in) | Signing, hashing for API auth | + +**Real-world examples using native packages:** + +- `packages/sources/view-function/` - ethers.js for EVM contract calls +- `packages/sources/view-function-multi-chain/` - ethers.js for multi-chain contract reads +- `packages/sources/token-balance/` - ethers.js + @solana/web3.js for token balances +- `packages/sources/uniswap-v2/` - ethers.js for DEX price reads + +**Key pattern:** Use native packages within the transport's `prepareRequests`/`parseResponse` or in a custom transport extending `SubscriptionTransport`. + +**Agent docs** +focus on EA code, DO NOT generate extra markdown to explain/summarize/report the agent behaviour including but not limited to report, summary, result, checklist, etc.... + +--- + +## 7. Framework Reference + +### Framework Root + +``` +.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/ +``` + +### Directory Map + +| Directory | Contents | Key Files | +| -------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `index.d.ts` | Main entry point | `expose`, `start`, `getTLSOptions` | +| `adapter/` | Adapters and endpoints | `basic.d.ts`, `endpoint.d.ts`, `price.d.ts`, `lwba.d.ts`, `por.d.ts`, `market-status.d.ts`, `stock.d.ts`, `types.d.ts` | +| `transports/` | Transport implementations | `http.d.ts`, `websocket.d.ts`, `sse.d.ts`, `metrics.d.ts`, `abstract/` (streaming, subscription) | +| `validation/` | Input params and validation | `input-params.d.ts` (InputParameters, EmptyInputParameters), `error.d.ts` (AdapterError) | +| `config/` | Config and settings | `index.d.ts` (AdapterConfig, BaseSettingsDefinition) | +| `util/` | Utilities and types | `types.d.ts` (SingleNumberResultResponse), `logger.d.ts`, `requester.d.ts`, `testing-utils.d.ts`, `subscription-set/`, `censor/` | +| `cache/` | Cache implementations | `local.d.ts`, `redis.d.ts`, `response.d.ts`, `factory.d.ts` | +| `rate-limiting/` | Rate limiting | `burst.d.ts`, `fixed-interval.d.ts`, `factory.d.ts` | +| `metrics/` | Prometheus metrics | `index.d.ts`, `constants.d.ts`, `util.d.ts` | +| `background-executor.d.ts` | Background execution | Background task runner | +| `debug/` | Debug endpoints | `router.d.ts`, `settings-page.d.ts` | +| `status/` | Status endpoint | `router.d.ts` | + +### Response Types + +Read `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/types.d.ts` for: + +- `SingleNumberResultResponse` +- `AdapterResponse` +- `ProviderResult` +- `ResponseTimestamps` + +Read `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/por.d.ts` for: + +- `PoRProviderResponse` +- `PoRAddressResponse` +- `PoRBalanceResponse` + +Read `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/lwba.d.ts` for: + +- `LwbaResponseDataFields` + +### Real-World Examples + +| Pattern | Example Packages | +| ------------------- | -------------------------------------------------------------------------- | +| HTTP Transport | `packages/sources/coingecko/`, `packages/sources/coinpaprika/` | +| WebSocket Transport | `packages/sources/ncfx/`, `packages/sources/tiingo/` | +| Empty Input Params | `packages/sources/wbtc-address-set/`, `packages/sources/the-network-firm/` | +| Price Endpoint | `packages/sources/coingecko/`, `packages/sources/cryptocompare/` | +| LWBA Endpoint | `packages/sources/ncfx/`, `packages/sources/gsr/` | +| PoR Endpoints | `packages/sources/the-network-firm/`, `packages/sources/wbtc-address-set/` | + +### Error Handling + +Read `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/validation/error.d.ts` for: + +- `AdapterError` - Base error class +- `AdapterInputError` - Input validation errors +- `AdapterRateLimitError` - Rate limit errors +- `AdapterDataProviderError` - Provider errors + +### Testing Utilities + +Read `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts` for test helpers. + +### How to Use + +1. **Read the `.d.ts` files** in the framework directory to understand types and interfaces +2. **Search existing EAs** in `packages/sources/` for implementation patterns +3. **Use grep** to find usage patterns: `grep -r "HttpTransport" packages/sources/` diff --git a/.claude/agents/ea_integration_test_validator.md b/.claude/agents/ea_integration_test_validator.md new file mode 100644 index 0000000000..dfb19d50e6 --- /dev/null +++ b/.claude/agents/ea_integration_test_validator.md @@ -0,0 +1,171 @@ +### Integration Test Validation Guide (External Adapter Framework) + +FOCUS ONLY ON SCOPE OF INTEGRATION TEST + +## Framework Reference + +**Before validating, understand the framework components:** + +| Component | Framework Source | What to Validate | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- | +| `TestAdapter` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts` | Proper usage of test harness | +| `HttpTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/http.d.ts` | Transport patterns used | +| `SubscriptionTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/subscription.d.ts` | Background execution handling | +| Response types | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/types.d.ts` | Response structure validation | + +**Read these framework files to understand:** + +- What `TestAdapter` provides +- What transport patterns are expected +- What response formats are standard + +### 1. Task + +- You are reviewing **only integration tests** for a single adapter in `packages/sources//test/integration/**`. +- Answer one question: + - **"Are these integration tests high quality, meaningful, effective, efficient, and complete end‑to‑end tests for this adapter?"** +- Return your verdict: + - **If YES: Set approved=true and leave rationale EMPTY (do not explain why it passed - saves tokens)** + - **If NO: Set approved=false and provide rationale explaining what needs to be fixed** +- Do **not** change any code (no edits to tests or sources). + +--- + +### 2. What counts as an integration test (EAF adapters) + +**Framework components to check:** + +- `TestAdapter` usage: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts` +- `MockWebsocketServer` usage: Same file +- Response types: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/types.d.ts` + +Treat a test as a _real_ integration test only if: + +- **It goes through the adapter entrypoint** + - Uses `TestAdapter` (from framework testing utils) and sends realistic adapter requests like `{ base, quote, endpoint }`. + - Check: Does it import and use framework's `TestAdapter`? +- **It uses realistic env + provider mocks** + - Env: only the vars the adapter actually needs (`API_KEY`, RPC URLs, WS URLs, etc.). + - `BACKGROUND_EXECUTE_MS` is around **10s (10000ms)**, not an aggressive polling interval. + - Providers are mocked (e.g. `nock`, `MockWebsocketServer` from framework, `SocketServerMock`), not the adapter itself. +- **It asserts on user-visible behaviour** + - Status codes, full JSON response (often snapshots), and key business fields (`result`, `data.result`, symbols, ripcord flags, etc.). + - Check: Does it validate `AdapterResponse` structure from framework? + +Tests that just call pure helpers or internal functions without going through the adapter entrypoint are **not** integration tests; call that out. + +--- + +### 3. What a good integration suite should cover + +When you look at the whole suite, check for these **signals of quality**: + +- **Happy paths for main endpoints** + + - Each major endpoint (`price`, `reserve`, `nav`, `crypto_lwba`, etc.) has at least one realistic success case. + - If there are clearly different request shapes (e.g. different `type`, symbol class, network), there is coverage for each shape that materially changes behaviour. + +- **Validation behaviour** + + - Empty body `{}` and missing required fields (`base`, `quote`, `endpoint`, etc.) → `400` with a sensible error. + - Bad combinations (e.g. unsupported network + endpoint) are handled and tested. + +- **Provider / upstream failures** + + - For important endpoints, at least one test shows behaviour when: + - Provider returns `4xx` / `5xx` + - Provider returns malformed / semantically bad data + - The adapter maps those to appropriate adapter errors (e.g. `502`/`504`) with useful JSON. + +- **Edge cases that matter** + + - Examples: zero balances, empty arrays, inverse pairs, stale prices, overrides/mapping behaviour. + - These tests should clearly exercise non‑trivial branches in the adapter. + +- **Transports and orchestration** + + **Framework transport types:** + + - `HttpTransport`: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/http.d.ts` + - `SubscriptionTransport`: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/subscription.d.ts` + + **Validation criteria:** + + - REST: at least one test goes all the way through real request building and nocked HTTP. + - WebSocket / streaming: + - Cache is warmed with `testAdapter.request(...)` + `waitForCache(...)` (from framework `TestAdapter`). + - `FakeTimers` / `runAllUntilTime` (from framework testing utils) are used where TTL / heartbeat logic matters. + - There is coverage for: successful subscription, a failure/invariant‑violation path, and (where relevant) subscription deduplication. + +- **Env / caching / rate limiting sanity** + - Env usage is explicit and minimal; tests don’t depend on hidden global env. + - For cache‑heavy/WS adapters, at least one test shows TTL/heartbeat or cache usage. + - Rate limiting is typically disabled for tests (`adapter.rateLimiting = undefined`) but does not hide important behaviour. + +If a suite obviously **skips an entire dimension** (e.g. no validation errors anywhere, or no upstream error coverage), lean toward **NO**. + +--- + +### 4. Low‑value patterns (should be called out) + +Flag these as **redundant / out‑of‑scope for integration**: + +- Tests that **do not go through** the adapter entrypoint. +- Tests of **language / framework trivia** (basic `try/catch`, trivial conditionals, array methods). +- Tests that exercise only **pure utilities** (parsers, formatters, math) without the adapter. +- Tests that focus mainly on **mock wiring details** instead of final adapter output. +- Large numbers of **near‑duplicate** tests with no additional behavioural insight. + +--- + +### 5. How to structure your answer + +**If approved (YES):** + +- Set `approved: true` and `rationale: ""` (empty string) +- Do NOT explain why it passed - this saves output tokens + +**If rejected (NO):** + +- Set `approved: false` +- Provide concise rationale with: + - Key gaps (2-3 bullets max) + - What needs to be fixed to pass +- Keep it short and actionable + +--- + +### 6. How the tests are expected to run (sanity check) + +Assume tests are run like this: + +```bash +cd external-adapters-js && yarn install && yarn setup +cd external-adapters-js && yarn clean && yarn build +export adapter=[adapter-name] +timeout 30 yarn test $adapter/test/integration +``` + +- All integration tests should **pass as written** under this flow. +- Flaky tests, real‑network dependencies, or undocumented extra setup are quality issues you should mention. + +--- + +### 7. Agent docs + +- Focus on **EA code and tests only**. +- DO NOT generate extra markdown to explain/summarize/report the agent behaviour, including but not limited to: + - Reports about how you followed these rules + - Meta‑checklists + - Tool‑usage logs + +Your output should be a concise, human‑readable evaluation of the adapter's **integration tests**, based on the standards above and the EAF integration patterns in `ea_integration_writer`. + +**When validating, verify:** + +1. Proper use of framework's `TestAdapter` from testing utils +2. Correct transport pattern based on adapter's transport type (check framework source) +3. Proper mocking using framework-provided utilities (`MockWebsocketServer`, etc.) +4. Response validation matches framework's response types + +**Always refer to framework source files** to understand expected patterns and usage. diff --git a/.claude/agents/ea_integration_test_writer.md b/.claude/agents/ea_integration_test_writer.md new file mode 100644 index 0000000000..d8bf0ae8dd --- /dev/null +++ b/.claude/agents/ea_integration_test_writer.md @@ -0,0 +1,1279 @@ +### External Adapter Framework–only runbook for integration tests + +## Framework Reference + +### Testing Utilities Location + +``` +.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts +``` + +**Key exports to use:** + +- `TestAdapter` - Main test harness for adapter testing +- `MockWebsocketServer` - WebSocket server mocking +- `setEnvVariables` - Environment variable management +- `runAllUntilTime` - FakeTimers advancement utilities +- `MockCache` - Cache implementation for tests + +**Read the framework source** for complete type definitions and usage patterns. + +### Transport Types Reference + +| Component | Framework Source | Use in Tests | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | +| `HttpTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/http.d.ts` | Understanding request/response flow | +| `WebSocketTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/websocket.d.ts` | WebSocket test patterns | +| `SubscriptionTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/subscription.d.ts` | Background execution patterns | +| `StreamingTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/streaming.d.ts` | Streaming data patterns | + +### Response Types Reference + +| Type | Framework Source | Use in Tests | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | +| `AdapterResponse` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/types.d.ts` | Response assertions | +| `SingleNumberResultResponse` | Same file | Price/numeric result assertions | +| `ProviderResult` | Same file | Provider data validation | + +### 1. Test file layout (EAF) + +- **Directory**: `packages/sources//test/integration/` +- **Files** + - **Core HTTP**: `adapter.test.ts` + - **Per-endpoint** (optional when complex): e.g. `totalBalance.test.ts`, `wallet.test.ts`, `nav.test.ts` + - **Websocket / streaming**: `adapter-ws.test.ts`, `.test.ts` + - **Fixtures**: `fixtures.ts` (and `fixtures-*.ts` for larger suites) + - **Utils** (optional): `utils/fixtures.ts`, `utils/testConfig.ts`, `utils/utilFunctions.ts` + +--- + +### 2. Standard EAF integration test skeleton + +**Reference the framework testing utilities:** + +- Location: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts` +- Read the `.d.ts` file for complete API and type definitions + +Use this as the template for each `*.test.ts`: + +```ts +import { + TestAdapter, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import * as nock from 'nock' +// import FakeTimers from '@sinonjs/fake-timers' // for WS / time-based tests +// import { WebSocketClassProvider } from '@chainlink/external-adapter-framework/transports' +// import { mockWebSocketProvider, MockWebsocketServer } from '@chainlink/external-adapter-framework/util/testing-utils' +import { mockUpstreamSuccess, mockUpstreamFailure } from './fixtures' // your fixtures + +describe('execute', () => { + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + let spy: jest.SpyInstance // Date.now, or remove if not needed + + beforeAll(async () => { + // snapshot env + oldEnv = JSON.parse(JSON.stringify(process.env)) + + // set only what's needed + process.env.API_KEY = process.env.API_KEY ?? 'test-api-key' + process.env.RPC_URL = process.env.RPC_URL ?? 'http://localhost:8545' + process.env.BACKGROUND_EXECUTE_MS = '0' // OK for mocked tests (common pattern) + // other adapter-specific envs... + + // freeze time for deterministic cache/snapshots + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + // import adapter and disable rate limiting when it interferes with tests + const adapter = (await import('../../src')).adapter + adapter.rateLimiting = undefined + + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + // clock: FakeTimers.install(), // for WS/time-based tests + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + nock.restore() + nock.cleanAll() + spy.mockRestore() + // testAdapter.clock?.uninstall() // if using FakeTimers + }) + + describe('happy path', () => { + it('returns success', async () => { + const data = { endpoint: 'price', base: 'ETH', quote: 'USD' } + mockUpstreamSuccess() + + const response = await testAdapter.request(data) + + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('validation errors', () => { + it('fails on missing base', async () => { + const response = await testAdapter.request({ + endpoint: 'price', + quote: 'USD', + }) + expect(response.statusCode).toBe(400) + expect(response.json()).toMatchSnapshot() + }) + + it('fails on empty request', async () => { + const response = await testAdapter.request({}) + expect(response.statusCode).toBe(400) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('upstream failures', () => { + it('maps upstream 5xx to adapter error', async () => { + mockUpstreamFailure() + const response = await testAdapter.request({ + endpoint: 'price', + base: 'ETH', + quote: 'USD', + }) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + }) +}) +``` + +#### Setup Breakdown + +1. **Environment Variable Snapshot**: Always snapshot before modifying + + ```ts + oldEnv = JSON.parse(JSON.stringify(process.env)) + ``` + +2. **Required Environment Variables**: Use fallback values (`??`) for optional env vars + + ```ts + process.env.API_KEY = process.env.API_KEY ?? 'fake-api-key' + process.env.BACKGROUND_EXECUTE_MS = '0' // OK for mocked tests + ``` + +3. **Time Mocking**: Essential for deterministic snapshots + + ```ts + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + ``` + +4. **Adapter Initialization**: Always disable rate limiting + + ```ts + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + ``` + +5. **Request Pattern**: Use `testAdapter.request()`, validate status code, use snapshots + ```ts + const response = await testAdapter.request({ base: 'ETH', quote: 'USD' }) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + ``` + +#### Cache Management (Advanced) + +For tests that need cache clearing between test cases: + +```ts +afterEach(() => { + nock.cleanAll() + const keys = testAdapter.mockCache?.cache.keys() + if (keys) { + for (const key of keys) { + testAdapter.mockCache?.delete(key) + } + } +}) +``` + +--- + +### 3. Testing SubscriptionTransport with Background Execution (CRITICAL) + +**For adapters using `SubscriptionTransport` (HTTP REST with polling OR WebSocket):** + +Subscription transports work differently from simple `HttpTransport`. They use background execution to periodically fetch data and populate a cache. The transport type determines your test pattern: + +#### Pattern 1: HTTP SubscriptionTransport with afterEach Cache Clearing (RECOMMENDED) + +**Best for HTTP REST adapters with SubscriptionTransport** (like `the-network-firm`, adapters with OAuth/auth state): + +```ts +describe('execute', () => { + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + // ... set env vars ... + + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterEach(() => { + // Clear nock mocks + nock.cleanAll() + + // Clear the EA cache between tests + const keys = testAdapter.mockCache?.cache.keys() + if (keys) { + for (const key of keys) { + testAdapter.mockCache?.delete(key) + } + } + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + nock.restore() + nock.cleanAll() + spy.mockRestore() + }) + + describe('endpoint tests', () => { + it('should return success', async () => { + const data = { endpoint: 'price', fundId: 8 } + mockApiResponse() + + // First call triggers background execution + await testAdapter.request(data) + // Second call retrieves cached result + const response = await testAdapter.request(data) + + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) +}) +``` + +**Why this works:** + +- `afterEach` clears both nock mocks AND cache after every test +- Each test starts with clean state (no cached data, no cached auth tokens) +- Double-call pattern ensures cache is populated before checking results, wait at least 10 seconds before second call +- Prevents test interference and non-deterministic errors + +**When to use:** + +- HTTP adapters with `SubscriptionTransport` +- Adapters with OAuth or authentication that caches tokens +- When tests may interfere with each other due to shared state + +#### Pattern 2: WebSocket with beforeAll Cache Warming + +**Best for WebSocket adapters** (like `tp`, `data-engine`, `finalto`, `twosigma`): + +```ts +describe('websocket', () => { + let mockWsServer: MockWebsocketServer | undefined + let testAdapter: TestAdapter + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.WS_API_ENDPOINT = wsEndpoint + + mockWebSocketProvider(WebSocketClassProvider) + mockWsServer = mockWebSocketServer(wsEndpoint) + + const adapter = (await import('./../../src')).adapter + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + clock: FakeTimers.install(), + testAdapter: {} as TestAdapter, + }) + + // Warm the cache: send initial requests and wait for cache to fill + await testAdapter.request(data1) + await testAdapter.request(data2) + await testAdapter.waitForCache(2) // Wait for N subscriptions + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + mockWsServer?.close() + testAdapter.clock?.uninstall() + await testAdapter.api.close() + }) + + describe('tests', () => { + it('should return success', async () => { + // Single call works - cache was warmed in beforeAll + const response = await testAdapter.request(data1) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) +}) +``` + +**Why this works:** + +- WebSocket connections are long-lived and shared across tests +- Warm cache once in `beforeAll`, reuse for all tests +- `waitForCache()` ensures background executor has populated cache +- No need to reconnect WebSocket for each test + +#### Pattern 3: Double-Call Pattern (LEGACY - Use afterEach Instead) + +**This pattern is older and less recommended** but may still be seen in some adapters: + +```ts +it('should return success', async () => { + const data = { endpoint: 'price', fundId: 8 } + mockApiResponse() + + // First call: triggers background executor + await testAdapter.request(data) + + // Second call: retrieves cached result + const response = await testAdapter.request(data) + + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() +}) +``` + +**Why it's less preferred:** + +- More verbose (2 calls per test) +- Doesn't prevent test interference +- Auth tokens/state still cached across tests +- Can cause non-deterministic snapshot failures + +**Prefer afterEach cache clearing (Pattern 1) instead.** + +#### Comparison: HttpTransport vs SubscriptionTransport + +| Transport Type | Background Execution | Test Pattern | Examples | +| -------------------------------- | -------------------- | ----------------------------- | ------------------------------------ | +| **HttpTransport** | No | Single call, no special setup | `streamex` | +| **SubscriptionTransport (HTTP)** | Yes | afterEach cache clearing | `the-network-firm`, `asseto-finance` | +| **SubscriptionTransport (WS)** | Yes | beforeAll cache warming | `tp`, `data-engine`, `finalto` | + +#### Determining Which Transport Your Adapter Uses + +**Read the framework transport definitions:** + +- `HttpTransport`: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/http.d.ts` +- `SubscriptionTransport`: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/subscription.d.ts` + +Check your adapter's transport file: + +```ts +// HttpTransport - No background execution +// Implements prepareRequests() and parseResponse() +export const httpTransport = new HttpTransport({...}) + +// SubscriptionTransport - Has background execution +// Implements backgroundHandler() method +class MyTransport extends SubscriptionTransport { + async backgroundHandler(context, entries) {...} +} +``` + +**Key identifiers:** + +- `HttpTransport` → No `backgroundHandler` → Use single-call pattern +- `SubscriptionTransport` → Has `backgroundHandler` → Use Pattern 1 (HTTP) or Pattern 2 (WebSocket) + +**If you see `SubscriptionTransport` and `backgroundHandler`**, use Pattern 1 (afterEach) or Pattern 2 (beforeAll warm-up). +**If you see `HttpTransport`**, single-call pattern with no special setup is fine. + +--- + +### 4. WebSocket / streaming–specific pattern (EAF) + +For WS endpoints (like `coinmetrics-lwba`, `dxfeed`, `tp`, `finalto`, etc.): + +- **Additional setup in `beforeAll`**: + +```ts +const wsEndpoint = 'ws://localhost:9090/your-path' + +let mockWsServer: MockWebsocketServer | undefined + +beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.WS_API_ENDPOINT = wsEndpoint + process.env.WS_SUBSCRIPTION_TTL = '5000' + process.env.CACHE_MAX_AGE = '5000' + process.env.CACHE_POLLING_MAX_RETRIES = '0' + process.env.WS_SUBSCRIPTION_UNRESPONSIVE_TTL = '180000' + process.env.API_KEY = 'fake-api-key' + + mockWebSocketProvider(WebSocketClassProvider) + mockWsServer = mockYourWebSocketServer(wsEndpoint) // from fixtures + + const adapter = (await import('../../src')).adapter + adapter.rateLimiting = undefined + + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + clock: FakeTimers.install(), + testAdapter: {} as TestAdapter, + }) + + // warm the cache so background execute starts + await testAdapter.request({ + endpoint: 'your-ws-endpoint' /* other params */, + }) + await testAdapter.waitForCache() // or waitForCache(n) for multiple subscriptions +}) + +afterAll(async () => { + setEnvVariables(oldEnv) + mockWsServer?.close() + testAdapter.clock?.uninstall() + await testAdapter.api.close() +}) +``` + +- **Tests**: + - Happy path: call `testAdapter.request(payload)` and snapshot. + - Validation: `{}`, missing fields, etc. -> expect `400`. + - Invariant violation / unusual messages: + - Advance fake clock: `testAdapter.clock.tick(1000)` or `await runAllUntilTime(testAdapter.clock, 91000)` + - Request again and snapshot. + - Heartbeat/TTL updates: advance clock and verify cache still valid. + +#### Socket.IO Pattern + +For adapters using `socket.io-client`: + +```ts +import { SocketServerMock } from 'socket.io-mock-ts' + +beforeAll(async () => { + const socket = new SocketServerMock() + jest.doMock('socket.io-client', () => ({ + io: () => socket, + })) + + // Emit mock events + socket.clientMock.emit('initial_token_states', [ + { + id: 'FRAX/USD', + baseSymbol: 'FRAX', + quoteSymbol: 'USD', + price: 0.9950774676498447, + }, + ]) + + // Continue with adapter setup... +}) +``` + +#### Multiple WebSocket Servers + +For adapters with multiple WebSocket endpoints: + +```ts +let mockWsServerCrypto: MockWebsocketServer | undefined +let mockWsServerForex: MockWebsocketServer | undefined + +beforeAll(async () => { + mockWsServerCrypto = mockCryptoWebSocketServer(wsEndpoint + '/crypto') + mockWsServerForex = mockForexWebSocketServer(wsEndpoint + '/forex') + // ... +}) + +afterAll(async () => { + mockWsServerCrypto?.close() + mockWsServerForex?.close() + // ... +}) +``` + +--- + +### 4. On-chain / complex dependency pattern + +For adapters heavily using `ethers`, `@solana/web3.js`, or custom classes (like `solana-functions`, `token-balance`, `por-address-list`): + +- **Mock external libraries** at top of test: + +```ts +jest.mock('ethers', () => { + const actual = jest.requireActual('ethers') + return { + ...actual, + ethers: { + ...actual.ethers, + providers: { + JsonRpcProvider: jest.fn().mockImplementation(() => ({ + getBlockNumber: jest.fn().mockResolvedValue(1000), + })), + }, + Contract: jest.fn().mockImplementation(() => ({ + decimals: jest.fn().mockResolvedValue(8), + latestAnswer: jest.fn().mockResolvedValue(5000000000n), + })), + }, + } +}) +``` + +or, for Solana: + +```ts +jest.mock('@solana/web3.js', () => ({ + PublicKey: jest.fn().mockImplementation(() => ({})), + Connection: jest.fn().mockImplementation(() => ({ + getAccountInfo: jest.fn().mockResolvedValue({ lamports: 123_000_000_000 }), + })), +})) +``` + +- **Class-level behavior** (e.g. account readers) is controlled via `jest.spyOn` and custom implementations per test case. + +--- + +### 5. Fixtures design (EAF) + +- **Inputs and outputs must strictly match** the YAML `required_ea_requests_responses_schemas`. +- Snapshot responses; they must **match the YAML-required schemas exactly**. Use the DP response provided by the YAML spec, no need to make a real request to the DP +- Put **all HTTP/WS mocks** into `fixtures.ts`: + - One function per scenario: `mockStakeSuccess`, `mockWalletListResponseSuccess`, `mockBedRockResponseSuccess`, etc. + - Use `nock` for REST calls and `MockWebsocketServer` helper for WS. set the nock in package.json + - Use `.persist()` for mocks that should survive multiple requests. +- Keep fixtures: + - **Deterministic** (fixed timestamps, no randomness). + - **Small enough** for readable snapshots; if payloads are large, normalize or pick key fields before snapshotting. + - **Reusable**: Create parameterized functions when possible (e.g., `mockResponseSuccess(assetId: string)`). + +#### ⚠️ CRITICAL: POST Request Fixture Common Pitfalls + +When mocking POST requests with JSON bodies, avoid these common issues: + +1. **Exact body matching fails due to key order differences** - JSON key ordering is not guaranteed in JavaScript +2. **Mocks consumed after one use** - Without `.persist()`, nock removes the mock after first match +3. **Dynamic fields in requests** - Request IDs, timestamps, or nonces change each request + +**These issues apply to ALL POST fixtures** + +```ts +// ❌ BAD - Exact body matching can fail due to key order differences +export const mockPostBad = (): nock.Scope => + nock('https://api.example.com') + .post('/data', { + type: 'query', + asset: 'ETH', + timestamp: 1234567890, + }) + .reply(200, { result: 'success' }) +// ❌ No .persist() - mock consumed after one use! + +// ✅ GOOD - Use .persist() and function-based body matcher if needed +export const mockPostGood = (): nock.Scope => + nock('https://api.example.com') + .persist() + .post('/data', (body) => body.type === 'query' && body.asset === 'ETH') + .reply(200, { result: 'success' }) + +// ✅ GOOD - For dynamic fields, use regex or function matchers +export const mockPostWithDynamicFields = (): nock.Scope => + nock('https://api.example.com') + .persist() + .post('/data', { + type: 'query', + asset: 'ETH', + requestId: /^[a-f0-9-]+$/, // Regex for dynamic UUID + }) + .reply(200, (_, requestBody: any) => ({ + requestId: requestBody.requestId, // Echo back dynamic field + result: 'success', + })) +``` + +**Key Fixture Best Practices:** + +| Practice | Why | +| ------------------------------------------ | --------------------------------------------------------------- | +| Always use `.persist()` | Mocks survive multiple requests (background execution, retries) | +| Use function body matchers `(body) => ...` | Avoids JSON key order issues | +| Use regex for dynamic fields `/^\d+$/` | Matches varying IDs, timestamps, nonces | +| Use function replies `(_, req) => ({...})` | Allows echoing back dynamic request fields | +| Log unmatched requests | Helps debug missing mock cases | + +#### Example HTTP GET fixture: + +```ts +export const mockResponseSuccess = (): nock.Scope => + nock('https://api.example.com', { encodedQueryParams: true }) + .persist() + .get('/api/price') + .query({ symbol: 'ETH', convert: 'USD' }) + .reply(200, () => ({ ETH: { price: 10000 } }), [ + 'Content-Type', + 'application/json', + 'Connection', + 'close', + ]) + +export const mockResponseFailure = (): nock.Scope => + nock('https://api.example.com', { encodedQueryParams: true }) + .persist() + .get('/api/price') + .query({ symbol: 'ETH', convert: 'USD' }) + .reply(500, { error: 'Internal Server Error' }) + +export const mockResponseWithHeaders = (): nock.Scope => + nock('https://api.example.com', { + encodedQueryParams: true, + reqheaders: { + 'x-api-key': 'fake-api-key', + Authorization: 'Bearer fake-token', + }, + }) + .persist() + .get('/api/data') + .reply(200, { data: 'success' }) +``` + +#### Example RPC/Blockchain fixture: + +RPC calls are POST requests and follow the same patterns above. Additionally, RPC has specific considerations: + +- **Dynamic `id` field** - JSON-RPC requests have `id` that changes each call +- **Batch requests** - Some providers batch multiple RPC calls into a single request +- **Function signature routing** - Contract calls need routing based on `data` field selector + +**Basic RPC Mock (Single Calls):** + +```ts +import { AdapterRequest } from '@chainlink/ea-bootstrap' + +export const mockRPCResponse = (): nock.Scope => + nock('https://test-rpc-url:443', { encodedQueryParams: true }) + .persist() + .post('/', { + method: 'eth_chainId', + params: [], + id: /^\d+$/, // Use regex for dynamic IDs + jsonrpc: '2.0', + }) + .reply(200, (_, request: AdapterRequest) => ({ + jsonrpc: '2.0', + id: request.id, // Echo back the request ID + result: '0x1', + })) + .post('/', { + method: 'eth_call', + params: [{ to: '0x...', data: '0x...' }, 'latest'], + id: /^\d+$/, + jsonrpc: '2.0', + }) + .reply(200, (_, request: AdapterRequest) => ({ + jsonrpc: '2.0', + id: request.id, + result: '0x0000000000000000000000000000000000000000000000000e1b77935f500bea', + })) +``` + +**Advanced Pattern: Dynamic Response Handler for Batch RPC** + +For adapters that make batch RPC calls or need dynamic responses based on method/params: + +```ts +type JsonRpcPayload = { + id: number + method: string + params: Array<{ to: string; data: string }> + jsonrpc: '2.0' +} + +const BALANCE_OF_SIG_HASH = '0x70a08231' +const TOTAL_SUPPLY_SIG_HASH = '0x18160ddd' + +export const mockEthereumRpc = (): nock.Scope => + nock('http://localhost:8545', {}) + // Match batch requests (array of RPC calls) + .post('/', (body: any) => Array.isArray(body)) + .reply( + 200, + (_uri, requestBody: JsonRpcPayload[]) => { + return requestBody.map((request: JsonRpcPayload) => { + if (request.method === 'eth_chainId') { + return { jsonrpc: '2.0', id: request.id, result: '0x1' } + } else if (request.method === 'eth_blockNumber') { + return { jsonrpc: '2.0', id: request.id, result: '0x15f5e10' } + } else if (request.method === 'eth_call') { + const [{ to, data }] = request.params + // Route based on function signature + if (data.startsWith(BALANCE_OF_SIG_HASH)) { + return { jsonrpc: '2.0', id: request.id, result: '0x...' } + } else if (data.startsWith(TOTAL_SUPPLY_SIG_HASH)) { + return { jsonrpc: '2.0', id: request.id, result: '0x...' } + } + } + // Log unmatched requests for debugging + console.log('Unmocked RPC request:', JSON.stringify(request, null, 2)) + return { jsonrpc: '2.0', id: request.id, result: '' } + }) + }, + ['Content-Type', 'application/json', 'Connection', 'close'], + ) + .persist() +``` + +**RPC-Specific Best Practices:** + +| Practice | Why | +| ----------------------------------- | ------------------------------------------------ | +| Use `id: /^\d+$/` regex | JSON-RPC request IDs are dynamic | +| Echo back `request.id` in response | Maintains request-response correlation | +| Use `(body) => Array.isArray(body)` | Catches batch RPC calls | +| Use signature hashes for routing | Match contract calls by 4-byte function selector | + +#### Example WebSocket fixture: + +**Reference MockWebsocketServer:** + +- Location: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts` +- Export: `MockWebsocketServer` from `'mock-socket'` package + +```ts +import { MockWebsocketServer } from '@chainlink/external-adapter-framework/util/testing-utils' + +export const mockWebsocketServer = (URL: string): MockWebsocketServer => { + const mockWsServer = new MockWebsocketServer(URL, { mock: false }) + mockWsServer.on('connection', (socket) => { + socket.on('message', (message) => { + const parsed = JSON.parse(message as string) + + // Handle subscription messages + if (parsed.type === 'subscribe') { + const { base, quote } = parsed + socket.send( + JSON.stringify({ + type: 'price_update', + base, + quote, + price: 1.0539, + timestamp: '2023-03-08T02:31:00.000Z', + }), + ) + } + + // Handle heartbeat + if (parsed.type === 'heartbeat') { + setTimeout(() => { + socket.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })) + }, 10000) + } + }) + }) + + return mockWsServer +} +``` + +#### Fixtures Best Practices + +1. **Deterministic Data**: Use fixed timestamps, no randomness +2. **Persistent Mocks**: Use `.persist()` for mocks that should survive multiple requests +3. **Dynamic Responses**: Use functions for responses that need to vary based on request +4. **Error Scenarios**: Create separate fixtures for different error cases +5. **Header Matching**: Use `reqheaders` for API key/auth validation +6. **Query Parameters**: Use `.query()` for GET requests with query strings +7. **Reusable Functions**: Create parameterized fixture functions for flexibility + +--- + +### 6. Test matrix you should always cover (EAF) + +Run test result +execute the tests + +```bash + cd external-adapters-js && yarn install && yarn setup + cd external-adapters-js && yarn clean && yarn build + export adapter=[adapter-name] + yarn test $adapter/test/integration +``` + +For each EAF endpoint you expose: + +- **Happy path** + - At least one test per distinct request "shape" (e.g., different `type`, `network`, `chainId`, etc.). + - Symbol overrides (when applicable). + - Multiple endpoints (if adapter has multiple). + - Expected status code: `200`. +- **Validation** + - Empty body `{}` / missing required fields / invalid combinations. + - Invalid data types. + - Expected status code: `400`. +- **Upstream failure** + - HTTP 5xx errors → adapter should return `502`. + - HTTP 4xx errors → adapter should return appropriate error. + - Invalid JSON responses / missing expected fields. + - Expected status codes: `502`, `504`, etc. +- **Edge cases** + - Boundary values (empty lists, zero balances, extreme decimals). + - Case sensitivity (if applicable). + - Inverse pairs (for price adapters). + - Stale data handling. + - Empty/null responses. + - For WS: subscription TTL + invariant-violation handling + heartbeat updates. + +#### Example Test Structure + +```ts +describe('execute', () => { + // ... setup ... + + describe('price endpoint', () => { + describe('happy path', () => { + it('should return success for valid pair', async () => { + mockResponseSuccess() + const response = await testAdapter.request({ + base: 'ETH', + quote: 'USD', + }) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + + it('should handle symbol overrides', async () => { + mockResponseSuccess() + const response = await testAdapter.request({ + base: 'ZZZ', + quote: 'USD', + overrides: { adapterName: { ZZZ: 'ETH' } }, + }) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('validation errors', () => { + it('should fail on empty request', async () => { + const response = await testAdapter.request({}) + expect(response.statusCode).toBe(400) + expect(response.json()).toMatchSnapshot() + }) + + it('should fail on missing base', async () => { + const response = await testAdapter.request({ quote: 'USD' }) + expect(response.statusCode).toBe(400) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('upstream failures', () => { + it('should handle 5xx errors', async () => { + mockResponseFailure() + const response = await testAdapter.request({ + base: 'ETH', + quote: 'USD', + }) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + + it('should handle invalid response format', async () => { + mockInvalidResponse() + const response = await testAdapter.request({ + base: 'ETH', + quote: 'USD', + }) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + }) + + describe('edge cases', () => { + it('should handle inverse pairs', async () => { + mockResponseSuccess() + const response = await testAdapter.request({ + base: 'IDR', + quote: 'USD', + }) + expect(response.statusCode).toBe(200) + expect(response.json()).toMatchSnapshot() + }) + + it('should handle stale prices', async () => { + mockStalePriceResponse() + const response = await testAdapter.request({ + base: 'JPY', + quote: 'USD', + }) + expect(response.statusCode).toBe(502) + expect(response.json()).toMatchSnapshot() + }) + }) + }) +}) +``` + +#### Testing Subscription Deduplication (WebSocket) + +```ts +it('should only subscribe once for same pair', async () => { + const dataLowercase = { base: 'eth', quote: 'usd' } + const dataUppercase = { base: 'ETH', quote: 'USD' } + + const response1 = await testAdapter.request(dataLowercase) + const response2 = await testAdapter.request(dataUppercase) + + expect(response1.json()).toMatchSnapshot() + expect(response2.json()).toMatchSnapshot() + + // Verify subscription set only has one entry + expect(transport.subscriptionSet.getAll()).toHaveLength(1) +}) +``` + +--- + +### 7. Key Best Practices + +#### Core Setup + +- **Always mock time**: `jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())` +- **Disable rate limiting**: `adapter.rateLimiting = undefined` +- **Use snapshots**: `expect(response.json()).toMatchSnapshot()` +- **Environment fallbacks**: `process.env.API_KEY = process.env.API_KEY ?? 'fake-api-key'` + +#### ⚠️ Background Execution Configuration + +**Understanding `BACKGROUND_EXECUTE_MS` in tests:** + +In the codebase, you'll see two patterns: + +**Pattern A: `BACKGROUND_EXECUTE_MS = '0'` (Common in tests)** + +- Used in many existing tests for simplicity +- Disables the delay between background execution cycles +- Safe for **mocked tests** where you control all upstream responses +- Faster test execution + +**Pattern B: `BACKGROUND_EXECUTE_MS = '10000'` (Production default)** + +- Matches production behavior +- Safer if tests ever run against real endpoints +- Prevents flooding upstream services + +**Recommendation:** + +- For **new tests with mocked fixtures**: Either pattern works, `'0'` is fine +- For **tests that might hit real endpoints** (integration/E2E): Use `'10000'` +- For **production**: Never change from default (10 seconds) + +```ts +// OK for mocked tests - Many existing tests use this +process.env.BACKGROUND_EXECUTE_MS = '0' + +// OK - Matches production behavior +process.env.BACKGROUND_EXECUTE_MS = '10000' + +// ❌ BAD for production - Will flood upstream services +// Only use non-zero low values if you know what you're doing +process.env.BACKGROUND_EXECUTE_MS = '10' +``` + +#### Transport-Specific Patterns + +**For SubscriptionTransport (HTTP REST with polling):** + +- **Use `afterEach` cache clearing**: Prevents test interference and non-deterministic errors +- **Single-call pattern**: With `afterEach` cleanup, single calls work fine +- **Clear both nock AND cache**: `nock.cleanAll()` + clear `testAdapter.mockCache` + +**For SubscriptionTransport (WebSocket):** + +- **Warm cache in `beforeAll`**: Call `await testAdapter.request()` + `await testAdapter.waitForCache()` +- **Use FakeTimers**: `FakeTimers.install()` for time-based tests +- **Single-call pattern**: Cache is warmed once, reused for all tests + +**For HttpTransport:** + +- **Single-call pattern**: No special setup needed +- **Optional `afterEach`**: Only if tests share state + +#### Organization + +- **Group tests by endpoint**: Use nested `describe` blocks +- **Test data organization**: Use constants (e.g., `TEST_SUCCESS_ASSET_ID`) +- **Fixture reusability**: Create parameterized fixture functions + +--- + +### 8. Advanced Testing Patterns + +**Framework Testing Utilities Reference:** + +- `deferredPromise`: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/index.d.ts` +- `sleep`: Same file +- Read the framework source for complete utility functions + +#### Testing Async/Batched Operations with Deferred Promises + +For adapters that make multiple async calls that need to be controlled step-by-step (like `stader-balance`): + +```ts +import { deferredPromise, sleep } from '@chainlink/external-adapter-framework/util' + +type DeferredCall = { + resolve: () => void + promise: Promise +} + +const pendingCalls: Record = {} + +// In your mock +jest.mock('ethers', () => ({ + Contract: function () { + return { + someAsyncMethod: jest.fn().mockImplementation((address) => { + if (!(address in pendingCalls)) { + const [promise, resolve] = deferredPromise() + pendingCalls[address] = { resolve: () => resolve(mockData[address]), promise } + } + return pendingCalls[address].promise + }), + } + }, +})) + +// In your test - control async flow step by step +it('should handle batched async operations', async () => { + const responsePromise = testAdapter.request(data) + + // Wait for first batch of calls + await sleep(50) + expect(Object.keys(pendingCalls)).toHaveLength(2) + Object.values(pendingCalls).forEach((call) => call.resolve()) + + // Wait for next batch + await sleep(50) + expect(Object.keys(pendingCalls)).toHaveLength(4) + Object.values(pendingCalls).forEach((call) => call.resolve()) + + const response = await responsePromise + expect(response.statusCode).toBe(200) +}) +``` + +#### Mocking Class Methods with jest.spyOn per Test + +For adapters that need different mock behaviors per test (like `solana-functions`): + +```ts +describe('endpoint', () => { + afterEach(() => { + spy.mockRestore() // Restore spy after each test + }) + + it('should error on failure', async () => { + const mockDate = new Date('2005-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + jest.spyOn(MyClass.prototype, 'fetchData').mockImplementation(async () => { + throw new Error('Simulated failure') + }) + + const response = await testAdapter.request({ address: '...' }) + expect(response.statusCode).toBe(502) + }) + + it('should succeed with valid data', async () => { + const mockDate = new Date('2006-01-01T11:11:11.111Z') // Different date to avoid cache + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + jest.spyOn(MyClass.prototype, 'fetchData').mockImplementation(async () => fakeData) + + const response = await testAdapter.request({ address: '...' }) + expect(response.statusCode).toBe(200) + }) +}) +``` + +**Note**: When using different mock behaviors per test, use a different mock date for each test to avoid cache hits. + +#### Testing Multiple Error Codes + +For adapters that return different error codes based on upstream response (like `r25`): + +```ts +describe('error codes', () => { + afterEach(() => { + nock.cleanAll() + }) + + it('should handle params missing error - causes 504', async () => { + mockNavResponseParamsMissing() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(504) + expect(response.json().error).toBeDefined() + }) + + it('should handle internal server error - causes 502', async () => { + mockNavResponseInternalServerError() + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(502) + expect(response.json().errorMessage).toBe('System busy, please try again later.') + }) +}) +``` + +#### Testing with Real Async Delays + +Sometimes you need actual async delays rather than just multiple calls: + +```ts +it('should handle delayed response', async () => { + mockNavResponse() + await new Promise((resolve) => setTimeout(resolve, 300)) // Wait for background execution + + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) +}) +``` + +--- + +### 10. Complex Test Utilities + +**Framework utilities reference:** + +- `TestAdapter`: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts` +- Read the framework source for `MockCache` and other test helpers + +For adapters with complex test scenarios, create utility files: + +```ts +// utils/utilFunctions.ts +import { TestAdapter } from '@chainlink/external-adapter-framework/util/testing-utils' + +export const clearTestCache = (testAdapter: TestAdapter) => { + const keys = testAdapter.mockCache?.cache.keys() + if (keys) { + for (const key of keys) { + testAdapter.mockCache?.delete(key) + } + } +} + +// utils/testConfig.ts +export const TEST_SUCCESS_ASSET_ID = 'KUSPUM' +export const TEST_FAILURE_ASSET_ID = 'INVALID' +export const TEST_URL = 'https://test-api.example.com' +``` + +--- + +### 11. Checklist for New Integration Tests + +- [ ] Created `test/integration/` directory +- [ ] Created `adapter.test.ts` with proper structure +- [ ] Created `fixtures.ts` with mock responses +- [ ] Set up `beforeAll` with environment variables +- [ ] Mocked time for deterministic snapshots +- [ ] Disabled rate limiting +- [ ] Set `BACKGROUND_EXECUTE_MS = '0'` (or `'10000'` if testing against real endpoints) +- [ ] Chose correct test pattern based on transport type: + - [ ] HttpTransport: Single-call pattern + - [ ] SubscriptionTransport (HTTP): afterEach cache clearing + - [ ] SubscriptionTransport (WebSocket): beforeAll cache warming +- [ ] Implemented happy path tests +- [ ] Implemented validation error tests +- [ ] Implemented upstream failure tests +- [ ] Implemented edge case tests +- [ ] All tests use snapshots for response validation +- [ ] Proper cleanup in `afterAll` +- [ ] Tests are deterministic and repeatable +- [ ] WebSocket tests use `FakeTimers` (if applicable) +- [ ] WebSocket cache is warmed with `waitForCache()` (if applicable) + +--- + +### 12. Examples Reference + +#### By Test Pattern: + +**WebSocket Adapters:** + +- `packages/sources/tiingo/test/integration/adapter-ws.test.ts` - Multiple WS servers, FakeTimers, heartbeat testing +- `packages/sources/tp/test/integration/adapter.test.ts` - WebSocket price adapter +- `packages/sources/finalto/test/integration/adapter.test.ts` - WebSocket multi-endpoint +- `packages/sources/data-engine/test/integration/adapter.test.ts` - WebSocket +- `packages/sources/dxfeed/test/integration/adapter-ws.test.ts` - WS with message routing + +**Socket.IO Adapters:** + +- `packages/sources/aleno/test/integration/adapter-socket.test.ts` - Socket.IO mocking pattern + +**REST with afterEach Cache Clearing:** + +- `packages/sources/the-network-firm/test/integration/adapter.test.ts` - Multi-endpoint, afterEach pattern +- `packages/sources/clear-bank/test/integration/adapter-batch.test.ts` - Batch requests + +**On-chain / RPC Mocking:** + +- `packages/sources/token-balance/test/integration/adapter.test.ts` - Multi-chain RPC mocking +- `packages/sources/stader-balance/test/integration/adapter.test.ts` - Complex ethers mock with deferred promises +- `packages/sources/cmeth/test/integration/fixtures.ts` - Batch RPC with dynamic routing +- `packages/sources/curve/test/integration/fixtures.ts` - RPC fixtures with function replies +- `packages/sources/enzyme/test/integration/fixtures.ts` - RPC fixtures + +**Solana / Non-EVM:** + +- `packages/sources/solana-functions/test/integration/adapter.test.ts` - Solana with class mocking per test + +**Error Code Testing:** + +- `packages/sources/r25/test/integration/error-codes.test.ts` - Testing multiple upstream error codes + +**Simple REST:** + +- `packages/sources/streamex/test/integration/adapter.test.ts` - Simple REST pattern +- `packages/sources/finage/test/integration/adapter.test.ts` - REST multi-endpoint + +--- + +### Summary + +If you follow this EAF-only template (file layout, `TestAdapter` bootstrap, env/time mocking, fixtures, and the test matrix), you'll be aligned with how the most recent `@sources` EAF adapters are tested and will be able to generate robust integration suites consistently. + +**Framework Reference:** + +- Testing utilities: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/testing-utils.d.ts` +- Transport types: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/` +- Response types: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/util/types.d.ts` + +**Key principles:** + +1. **Use `TestAdapter`** from `@chainlink/external-adapter-framework/util/testing-utils` +2. **Deterministic tests** through time mocking and fixed data +3. **Comprehensive coverage** of happy paths, errors, and edge cases +4. **Proper fixtures** with reusable mock functions +5. **Clean setup/teardown** with environment variable management +6. **Snapshot testing** for response validation +7. **Fake timers** for WebSocket/time-based tests +8. **Do NOT** test unit tests scenarios, language/framework basics, mocks themselves, or trivial/non-business behaviors. + +**Always read the framework `.d.ts` files** for up-to-date type definitions and API details. diff --git a/.claude/agents/ea_unit_test_validator.md b/.claude/agents/ea_unit_test_validator.md new file mode 100644 index 0000000000..f1f9310f08 --- /dev/null +++ b/.claude/agents/ea_unit_test_validator.md @@ -0,0 +1,126 @@ +# Chainlink External Adapters (EAs) Unit Test Validation Guide + +## Framework Reference + +**Before validating, understand framework boundaries:** + +Read framework source to distinguish framework orchestration from custom business logic: + +| Framework Component | Location | Framework-Provided (Not Unit Testable) | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | +| Transport orchestration | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/` | Request batching, caching, retry logic | +| Input validation | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/validation/` | Schema validation, type checking | +| Adapter lifecycle | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/` | Endpoint registration, initialization | +| Background execution | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/subscription.d.ts` | Background handler orchestration | + +**Unit testable (custom business logic):** + +- Pure transformation functions +- Custom validators/parsers +- Business calculations +- Data formatters +- Authentication helpers + +**When validating:** + +1. Check if tests are testing framework behavior → Flag as invalid +2. Check if tests require framework instances → Flag as integration test scope +3. Check if tests are for isolated custom logic → Valid unit test + +## GOAL + +Analyze the unit tests to ensure they are isolated, indepenedent and ONLY testing MEANINGFUL BUSINESS LOGIC behaviour, NOT CONCEPTUAL/LANGUAGE BEHAVIOR, validate unit testing results, gaps and redundants in unit tests +to answer this quesiton: +Are these high quality, meanful, effective, efficient, and complete unit tests? + +## Non-Negotiable Principles + +- CRITICAL: ONLY Consider the unit test scope! +- Answer with YES (approved=true) or NO (approved=false) +- **If YES: Set approved=true and leave rationale EMPTY (do not explain why it passed)** +- **If NO: Set approved=false and provide rationale explaining what needs to be fixed** +- Not testing language or framework behaviors, conceptual, basics, mock definitions, or any other non meaningful tests to the user. +- NOT testing framework orchestration (read framework source at `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/` to identify) +- Each unit tests are isolated, independent, smallest unit, testing behaviour +- ALL IMPLEMENTATION OF BUSINESS LOGIC HAVE TO BE TESTED +- ONLY VALIDATION the test meaningfulness, DO NOT MODIFY & REMOVE ANY CODE FILES +- Always prioritize readable, maintainable tests over comprehensive coverage. + +**Validation checklist:** + +- [ ] Tests import pure functions, not framework classes (Adapter, Transport, etc.) +- [ ] Tests don't instantiate framework components +- [ ] Tests focus on custom business logic, not framework behavior +- [ ] Refer to framework source to verify distinction + +## Unit Test vs Integration Test Scope + +### ✅ UNIT TEST SCOPE (Test These in unit/) + +Test **isolated, custom business logic** that can run without framework instances: + +**Reference framework source to identify custom vs framework code:** + +- Framework transports: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/` +- Framework validation: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/validation/` +- Framework adapters: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/` + +**Unit testable custom logic:** + +- **Pure transformation functions**: Data parsing, formatting, conversion logic +- **Custom validation logic**: Business rules, data quality checks, staleness validation +- **Utility functions**: Helper functions exported from util files +- **Custom class methods**: Authentication managers, rate limiters, custom calculators +- **Response parsers**: Functions that transform provider responses (when exported for testing) +- **Error handling logic**: Custom error detection and message formatting + +**Characteristics:** + +- Can be imported and tested directly +- No framework dependencies (Adapter, Transport, InputParameters instances) +- Deterministic output for given input +- Isolated from external systems (use mocks for HTTP/DB) + +### 🚫 OUT OF SCOPE FOR UNIT TESTS + +Do **not** expect or require unit tests to cover any logic that depends on the EA framework runtime. + +**Framework-provided behavior (verify by reading framework source):** + +Read these framework files to understand what's provided: + +- Transport base classes: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/http.d.ts` +- Subscription transport: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/subscription.d.ts` +- Input validation: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/validation/input-params.d.ts` +- Adapter base: `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/basic.d.ts` + +**Examples of framework behavior (NOT unit testable):** + +- Transport orchestration (building/batching requests, routing, retry plumbing) +- Framework-provided validation (schema enforcement, parameter aliases, adapter defaults) +- Adapter or endpoint registration, configuration wiring, environment setup +- Lifecycle flows across transports, caching, warmup/background execution +- Network mechanics (actual HTTP requests, retries, timeouts) +- Any behaviour that needs instantiated framework classes to execute + +These behaviours must be verified by **integration tests** instead. When assessing UNIT tests, intentionally ignore gaps in the above areas—they are outside the unit-test contract. + +### 🎯 Key Decision Rule + +**Ask yourself: "Can I test this function/class by importing it directly and calling it, without instantiating Adapter/Transport/InputParameters?"** + +- **YES** → Unit test (isolated business logic) +- **NO** → Integration test (requires framework context) + +**When in doubt:** + +1. Read the framework source file for the component type +2. Check if the behavior is provided by the framework base class +3. Check if testing requires framework instance methods +4. If any of the above is true → NOT a unit test + +**Framework source reference:** +`.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/` + +**Agent docs** +focus on EA code, DO NOT generate extra markdown to explain/summarize/report the agent behaviour including but not limited to report, summary, result, checklist, etc.... diff --git a/.claude/agents/ea_unit_test_writer.md b/.claude/agents/ea_unit_test_writer.md new file mode 100644 index 0000000000..9b1295db23 --- /dev/null +++ b/.claude/agents/ea_unit_test_writer.md @@ -0,0 +1,105 @@ +# Chainlink External Adapter UNIT Test Guide + +You are a typescript expert specialized in writing high quality unit tests following testing best practice that are isolated, independent and meaningful. + +## Framework Reference + +**Understanding what to unit test:** + +Read framework source to understand what's framework-provided vs custom business logic: + +| Framework Component | Location | Not Unit Testable (Framework Handles) | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | +| `HttpTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/http.d.ts` | Request batching, caching, retries | +| `SubscriptionTransport` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/transports/abstract/subscription.d.ts` | Background execution orchestration | +| `InputParameters` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/validation/input-params.d.ts` | Schema validation, type checking | +| `AdapterEndpoint` | `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/adapter/endpoint.d.ts` | Endpoint routing, registration | + +**What IS unit testable (custom business logic):** + +- Pure functions in `transport/` that transform data +- Custom validators/parsers exported from utilities +- Business logic helpers (staleness checks, price calculations, etc.) +- Authentication/signing functions +- Data formatters/normalizers + +**Read the framework source** to distinguish framework orchestration from your custom logic. + +## Goal + +Wrtie and pass only meaningful, isolate, and independent unit tests of actual business logic implementation behaviour. + +## Non-Negotiable Principles + +- Test only meaningful business logic implementation behaviour, these are usually implemented in transport +- CRITICAL: Each unit tests are ONLY isolated, independent, smallest piece of logic, THIS IS NOT INTEGRATION TEST, NEVER test orchestrator/aggregator methods that coordinate multiple isolated pieces +- DO NOT test language or framework behaviors, conceptual, basics, mock definitions, or any other non meaningful tests to the user. +- DO NOT test framework-provided orchestration (read framework source to identify what's framework vs custom) +- DO NOT delete/skip/remove tests; DO refactor logic into testable utilities instead. +- Cover endpoint, transport, auth flow, helper, and utility. +- Prove tests are valuable, effective, efficient, non-redundant +- Always prioritize readable, maintainable tests over comprehensive coverage. +- Do not break existing **integration tests**; ensure both unit and integration tests pass. + +**Framework vs Custom Logic:** +Before writing any test, ask: "Is this framework orchestration or custom business logic?" + +- If it's calling framework methods (like `Transport.responseCache`, `Adapter.initialize`), it's NOT unit testable +- If it's a pure function you wrote (data transformation, calculation, validation), it IS unit testable + +Read the framework `.d.ts` files to understand what the framework provides. + +## Workflow + +1. Analyze the code + - Read framework source to understand what's framework-provided + - Verify directory structure and test mirrors. + - Flag missing or empty test files and directories. +2. Map every independent business logic + - Locate isolated, independent business logic (NOT framework orchestration) + - Note complex flows and identify their smallest isolated independent components + - Check: Can this function be imported and tested without framework instances? +3. audit existing tests + - Ensure each logic component has a dedicated test file. + - Confirm tests import and exercise the real implementation (not abstractions) + - Verify tests don't test framework behavior +4. Write Unit Tests + - **BEFORE writing ANY test, ask: "Does this method call other methods?" If YES → skip it, test the called methods instead** + - **Ask: "Does this use framework classes?" If YES → this is integration test scope, not unit test** + - Focus on business logic behavior, not conceptual + - Test smallest isolated independent logic, not aggregated class/function/utils + - Cover all endpoints, transports, auth flows, utilities, and helpers + - Extract complicated logic into pure utilities where needed for testability + - Test the item not object, for example + ```json + { + "key_1": 2309234, + "key_2": "this is a string" + } + ``` + have to test the result is expect to be key_1 equal to 2309234 after business logic +5. Run test result + - execute the tests + ```bash + cd external-adapters-js && yarn install && yarn setup + cd external-adapters-js && yarn clean && yarn build + export adapter=[adapter-name] + yarn test $adapter/test/unit + ``` +6. Refactor Implementation Code to pass the unit tests for production + +**Framework Source Reference:** + +- Check `.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/node_modules/@chainlink/external-adapter-framework/` to understand what's framework-provided +- Focus unit tests on YOUR custom logic, not framework orchestration + +## Best Practices + +- Apply KISS: keep implementation and tests simple. +- Group tests by logic/functions, not types/use cases +- Store deterministic payload fixtures (e.g., `test-payload.json`). +- Use idempotent operations where possible; be mindful of retries. +- Neutralize time/network randomness via frozen clocks and stable mocks. + +**Agent docs** +focus on EA code, DO NOT generate extra markdown to explain/summarize/report the agent behaviour including but not limited to report, summary, result, checklist, etc.... diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.github/workflows/generate-ea.yml b/.github/workflows/generate-ea.yml new file mode 100644 index 0000000000..a803c1a94d --- /dev/null +++ b/.github/workflows/generate-ea.yml @@ -0,0 +1,263 @@ +# Workflow: Triggers when a PR contains YAML in ea-agent/requests +# This is decoupled from JIRA - can be triggered by: +# 1. JIRA (via Workflow 1) - WIP +# 2. Manual PR creation by developer - This workflow +# 3. Comment "/generate-ea" on a PR - This workflow + +name: 'Generate EA from YAML' + +on: + pull_request: + types: [opened] + paths: + - 'ea-agent/requests/**/*.yaml' + - 'ea-agent/requests/**/*.yml' + issue_comment: + types: [created] + +jobs: + generate-ea: + runs-on: ubuntu-latest + # Run on PR events OR when "/generate-ea" comment is posted on a PR + if: | + github.event_name == 'pull_request' || + (github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(github.event.comment.body, '/generate-ea')) + permissions: + contents: write + pull-requests: write + id-token: write # Required for GCP authentication + + steps: + - name: Check commenter permissions + if: github.event_name == 'issue_comment' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: context.actor + }); + if (!['admin', 'write'].includes(permission.permission)) { + core.setFailed('Only collaborators with write access can trigger this workflow'); + } + console.log(`User ${context.actor} has ${permission.permission} permission`); + + - name: Get PR details (for comment trigger) + if: github.event_name == 'issue_comment' + id: pr + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number + }); + core.setOutput('head_sha', pr.head.sha); + core.setOutput('head_ref', pr.head.ref); + core.setOutput('number', pr.number); + + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Use SHA for immutable reference (prevents TOCTOU attacks) + ref: ${{ github.event_name == 'issue_comment' && steps.pr.outputs.head_sha || github.event.pull_request.head.sha }} + fetch-depth: 0 + token: ${{ github.token }} + + - name: Install uv + uses: astral-sh/setup-uv@e4db8464a088ece1b920f60402e813ea4de65b8f # v4 + + - name: Set up Python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.11' + + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5 # v2.1.12 + with: + credentials_json: ${{ secrets.CC_GHA_GCP_SERVICE_ACCOUNT_KEY }} + create_credentials_file: true + export_environment_variables: true + env: + # Ensure credentials are created outside the git repository + GOOGLE_APPLICATION_CREDENTIALS_FILE_PATH: /tmp/gcp-credentials.json + + - name: Configure environment for Claude Code + run: | + echo "CLAUDE_CODE_USE_VERTEX=1" >> $GITHUB_ENV + echo "CLOUD_ML_REGION=us-east5" >> $GITHUB_ENV + echo "ANTHROPIC_VERTEX_PROJECT_ID=${{ secrets.CC_GHA_GCP_PROJECT_ID }}" >> $GITHUB_ENV + echo "DISABLE_PROMPT_CACHING=0" >> $GITHUB_ENV + echo "DISABLE_TELEMETRY=1" >> $GITHUB_ENV + echo "DISABLE_ERROR_REPORTING=1" >> $GITHUB_ENV + echo "DISABLE_BUG_COMMAND=1" >> $GITHUB_ENV + echo "CI=true" >> $GITHUB_ENV + echo "TERM=dumb" >> $GITHUB_ENV + echo "NO_COLOR=1" >> $GITHUB_ENV + echo "FORCE_COLOR=0" >> $GITHUB_ENV + echo "DEBIAN_FRONTEND=noninteractive" >> $GITHUB_ENV + + - name: Configure Git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Get YAML file from PR + id: yaml + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + // Determine PR number based on trigger type + const prNumber = context.eventName === 'issue_comment' + ? context.issue.number + : context.payload.pull_request.number; + + // Get all files changed in this PR + const files = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + console.log(`Found ${files.data.length} files in PR`); + + // Find YAML file in ea-requests/ + const yamlFile = files.data.find(f => + f.filename.startsWith('ea-requests/') && + (f.filename.endsWith('.yaml') || f.filename.endsWith('.yml')) && + f.status !== 'removed' + ); + + if (!yamlFile) { + core.setFailed('No YAML file found in ea-requests/'); + return; + } + + console.log(`Found YAML file: ${yamlFile.filename}`); + + // Extract JIRA key from filename (e.g., ea-requests/EA-123.yaml -> EA-123) + const filename = yamlFile.filename.split('/').pop(); + const jiraKey = filename.replace(/\.ya?ml$/, ''); + + core.setOutput('file', yamlFile.filename); + core.setOutput('jira_key', jiraKey); + + console.log(`JIRA Key: ${jiraKey}`); + + - name: Display found YAML + run: | + echo "📄 YAML File: ${{ steps.yaml.outputs.file }}" + echo "🎫 JIRA Key: ${{ steps.yaml.outputs.jira_key }}" + echo "" + echo "=== YAML Contents ===" + cat "${{ steps.yaml.outputs.file }}" + + - name: Install dependencies + working-directory: ea-agent + run: | + uv sync --frozen + + - name: Install Node.js + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version: '22' + + - name: Install Claude Code + run: | + npm install -g @anthropic-ai/claude-code + + - name: Setup EA environment + run: | + ./ea-agent/scripts/setup-ea-env.sh + + - name: Run EA workflow + id: ea + run: | + echo "🚀 Starting EA generation..." + uv run --project ea-agent python ea-agent/src/source_ea_agent.py "${{ steps.yaml.outputs.file }}" + + - name: Check for changes + id: changes + run: | + # Remove any sensitive files that might have been created + find . -name "gha-creds-*.json" -delete || true + find . -name "*-credentials.json" -delete || true + + # Check for both modified files and untracked files + MODIFIED_FILES=$(git diff --name-only) + UNTRACKED_FILES=$(git ls-files --others --exclude-standard) + + echo "Modified files: $MODIFIED_FILES" + echo "Untracked files: $UNTRACKED_FILES" + + if [ -z "$MODIFIED_FILES" ] && [ -z "$UNTRACKED_FILES" ]; then + echo "No changes were made" + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "Changes detected" + echo "has_changes=true" >> $GITHUB_OUTPUT + + # Show what files have changed (for debugging) + echo "Files that changed:" + git status --porcelain + fi + + - name: Commit generated code + if: steps.changes.outputs.has_changes == 'true' + id: commit + run: | + git add -A + + git commit -m "feat(${{ steps.yaml.outputs.jira_key }}): Generated EA code" + git push + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "✅ Changes committed and pushed" + + - name: Update PR description + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 + with: + script: | + const jiraKey = '${{ steps.yaml.outputs.jira_key }}'; + const yamlFile = '${{ steps.yaml.outputs.file }}'; + const hasChanges = '${{ steps.changes.outputs.has_changes }}' === 'true'; + + // Determine PR number based on trigger type + const prNumber = context.eventName === 'issue_comment' + ? context.issue.number + : context.payload.pull_request.number; + + const body = `## 📋 EA Request + + | Field | Value | + |-------|-------| + | **JIRA Issue** | ${jiraKey} | + | **YAML File** | \`${yamlFile}\` | + + --- + + ✅ **EA Generation Complete!** + + ${hasChanges ? 'Generated code has been committed to this PR.' : 'No new code changes were generated.'} + + ### Review Checklist + - [ ] Code structure is correct + - [ ] Tests pass + - [ ] Configuration is correct + - [ ] Documentation is adequate + + --- + _Ready for review and merge._`; + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + body: body + }); + + console.log('✅ PR description updated'); diff --git a/ea-agent/README.md b/ea-agent/README.md new file mode 100644 index 0000000000..b410fd7677 --- /dev/null +++ b/ea-agent/README.md @@ -0,0 +1,233 @@ +# Source EA Agent + +The AI Agent to create Source External Adapters from YAML specifications. + +## How It Works + +The agent uses the [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code/sdk) to orchestrate the following workflow: + +``` +JIRA Request → JIRA Rovo Agent Spec Validator → YAML Spec → Developer → Code Review → Integration Tests → Unit Tests → Done +``` + +```mermaid +flowchart TD + J[JIRA Request] --> R[Rovo Agent Spec Validator] + R --> A[YAML Spec] + A --> B[EA Developer] + B --> C[Code Reviewer] + C -->|rejected| B + C -->|approved| D[Integration Test Writer] + D --> E{Validator} + E -->|rejected| D + E -->|3 approvals| F[Unit Test Writer] + F --> G{Validator} + G -->|rejected| F + G -->|3 approvals| H[Done] +``` + +| Phase | What Happens | +| ------------------------ | ---------------------------------------------------------------------------------------- | +| **1. Development** | Scaffolds EA with `yarn new`, implements transports/endpoints using framework components | +| **2. Code Review** | Validates code quality; loops back to developer if rejected | +| **3. Integration Tests** | Writes tests, validates with 3 approval rounds | +| **4. Unit Tests** | Writes tests, validates with 3 approval rounds | + +## Quick Start + +1. Get the YAML spec from JIRA (see [JIRA → YAML Spec](#jira--yaml-spec-ea-spec-validator) below) +2. Create a new branch and add the YAML file: + +```bash +git checkout -b feat/OPDATA-123-my-adapter +cp my-spec.yaml ea-agent/requests/OPDATA-123-my-adapter.yaml +git add ea-agent/requests/OPDATA-123-my-adapter.yaml +git commit -m "feat: Add EA request for my-adapter" +git push origin feat/OPDATA-123-my-adapter +``` + +3. Open a PR — the GitHub Actions workflow runs automatically +4. Wait for the agent to generate the EA code and commit it to the PR +5. Review and merge + +## JIRA → YAML Spec (EA Spec Validator) + +Before the EA Agent runs, requirements must be validated and converted to YAML using the [EA Spec Validator](https://smartcontract-it.atlassian.net/wiki/x/M4Bhew) Rovo Agent. + +### Setup the JIRA Ticket + +1. Set ticket type = **Feed Deployment** +2. Set OP Product Type = **External Adapter** +3. Provide EA requirements in the **Description** section + +### Trigger the Spec Validator + +**Option 1: Label trigger (recommended)** + +- Add `ai-spec-review` to the ticket's label field +- JIRA automation triggers the Rovo Agent +- YAML specs appear as a comment in 1-3 minutes + +**Option 2: Rovo Chat** + +- Navigate to the ticket (ensure URL ends with clean ticket ID, e.g., `/browse/OPDATA-3669`) +- Click "Ask Rovo" in the upper right +- Search for "EA Spec Validator" +- Click "Generate the YAML specs" + +### Edit and Use the YAML + +Once generated, click the edit icon on the comment to refine the YAML, then copy it to `ea-agent/requests/` in a new PR to trigger the EA Agent. + +See [examples/yaml-spec-template.yaml.template](examples/yaml-spec-template.yaml.template) for the template and [examples/example-yaml-spec-OPDATA-4790.yaml](examples/example-yaml-spec-OPDATA-4790.yaml) for a complete example. + +## GitHub Actions + +The agent runs automatically via `.github/workflows/generate-ea.yml`. + +### Required Secrets + +| Secret | Description | +| -------------------------------- | -------------------------------------------------- | +| `CC_GHA_GCP_SERVICE_ACCOUNT_KEY` | GCP service account credentials JSON for Vertex AI | +| `CC_GHA_GCP_PROJECT_ID` | GCP project ID for Vertex AI | + +### Trigger Options + +1. **Add YAML to PR** — Push a YAML file to `ea-agent/requests/` +2. **Comment** — Type `/generate-ea` on any PR with a YAML file + +### What Happens + +1. Detects YAML in `ea-agent/requests/` +2. Runs `ea-agent/scripts/setup-ea-env.sh` to install deps and unplug framework +3. Runs all 4 phases +4. Commits generated code to PR + +## How It Uses the EA Framework + +The agent generates EAs using **[@chainlink/external-adapter-framework](https://www.npmjs.com/package/@chainlink/external-adapter-framework)**. + +### Unplugging the Framework + +This repo uses [Yarn PnP](https://yarnpkg.com/features/pnp) where packages are stored in compressed `.zip` files inside `.yarn/cache/`. AI agents cannot read zip contents directly. + +In GitHub Actions, the setup script (`ea-agent/scripts/setup-ea-env.sh`) runs: + +```bash +yarn unplug @chainlink/external-adapter-framework +``` + +This extracts the framework to disk: + +``` +.yarn/unplugged/@chainlink-external-adapter-framework-npm-*/ + node_modules/@chainlink/external-adapter-framework/ + ├── *.d.ts # Type definitions the agent reads + ├── transports/ + ├── adapter/ + └── ... +``` + +The EA Developer agent can then read the `.d.ts` files to understand available components and implement the adapter correctly. + +### Scaffolding with `yarn new` + +The EA Developer agent runs `yarn new source` to scaffold a new adapter package. This command: + +1. Generates the package structure at `packages/sources/example-adapter/` +2. Creates boilerplate files (tsconfig, package.json, src/index.ts) +3. Agent then renames the folder to the requested adapter name +4. Runs `yarn new tsconfig` to register the package + +### Generated Structure + +``` +packages/sources// +├── src/ +│ ├── index.ts # Adapter with expose() +│ ├── config/index.ts # AdapterConfig +│ ├── endpoint/*.ts # Endpoints +│ └── transport/*.ts # Transports +├── test/ +│ ├── integration/ +│ └── unit/ +└── test-payload.json +``` + +## Interactive Use + +Reference agent prompts directly in Cursor with `@` mentions: + +``` +@ea_developer.md Scaffold an EA for packages/sources/my-adapter +``` + +| Agent | File | Purpose | +| --------------- | ------------------------- | -------------------- | +| Developer | `@ea_developer.md` | Scaffold new adapter | +| Reviewer | `@ea_code_reviewer.md` | Review code quality | +| Test Writers | `@ea_*_test_writer.md` | Write tests | +| Test Validators | `@ea_*_test_validator.md` | Validate tests | + +## How to Update Agent Behaviors + +Agent behaviors are defined by system prompts in `.claude/agents/`. Edit these files to change how agents work: + +| File | Controls | +| ---------------------------------- | ---------------------------------------------------------------------- | +| `ea_developer.md` | How EAs are scaffolded, framework component selection, coding patterns | +| `ea_code_reviewer.md` | Code review criteria, what passes/fails review | +| `ea_integration_test_writer.md` | Integration test patterns and structure | +| `ea_integration_test_validator.md` | Integration test validation criteria | +| `ea_unit_test_writer.md` | Unit test patterns and structure | +| `ea_unit_test_validator.md` | Unit test validation criteria | + +### Local Development + +To test prompt changes locally before pushing: + +**Prerequisites:** + +1. Set up Claude Code in dev container — follow the [Claude Code Local Setup Guide](https://github.com/smartcontractkit/claude-code-local-artifacts) +2. Python 3.11+ with [uv](https://github.com/astral-sh/uv) + +**Run locally (inside dev container):** + +```bash +ea-agent/scripts/setup-ea-env.sh +cd ea-agent && uv sync +uv run python src/source_ea_agent.py requests/OPDATA-123-my-adapter.yaml +``` + +**Environment Variables:** + +| Variable | Default | Description | +| ------------------ | -------------------------- | ---------------------------- | +| `WORKFLOW_MODEL` | `claude-opus-4-5@20251101` | Model to use | +| `VERBOSE_LOGGING` | `true` | Log all agent messages | +| `JSON_LOG_PATH` | — | Path for streaming JSON logs | +| `SUMMARY_LOG_PATH` | — | Path for final summary JSON | + +### Tips + +- Keep prompts focused and specific +- Add examples of good/bad patterns +- Reference existing EAs in `packages/sources/` as examples +- Test prompt changes locally before pushing to CI + +## Project Structure + +``` +ea-agent/ +├── src/source_ea_agent.py # Main orchestrator +├── scripts/setup-ea-env.sh # CI environment setup +├── examples/ # YAML templates and examples +└── requests/ # YAML requirement files (input) + +.claude/agents/ +├── ea_developer.md # Development agent prompt +├── ea_code_reviewer.md # Code review agent prompt +├── ea_integration_test_*.md # Integration test agents +└── ea_unit_test_*.md # Unit test agents +``` diff --git a/ea-agent/examples/example-yaml-spec-OPDATA-4790.yaml b/ea-agent/examples/example-yaml-spec-OPDATA-4790.yaml new file mode 100644 index 0000000000..682e592281 --- /dev/null +++ b/ea-agent/examples/example-yaml-spec-OPDATA-4790.yaml @@ -0,0 +1,129 @@ +adapter_name: xusd-usd-exchange-rate +adapter_type: source +description: External Adapter for fetching the round value from the XUSD contract on Ethereum for XUSD-USD exchange rate (Customer = Stream) + +# No hardcoded base_url - node operators provide their own RPC endpoint via environment variable + +data_source_authentication: + type: none + +contract_details: + # Default values shown for reference - actual values come from input parameters + default_address: '0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94' + default_function_selector: '0x146ca531' + network: ethereum-mainnet + chain_id: 1 + etherscan_url: https://etherscan.io/address/0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94#code + +data_source_endpoints: + - name: round + method: POST + path: / + description: Fetches the round value from the XUSD contract via eth_call JSON-RPC + parameters: + required: + - name: contractAddress + type: string + description: The Ethereum contract address to call + example: '0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94' + - name: functionSelector + type: string + description: The 4-byte function selector (keccak256 hash of function signature) + example: '0x146ca531' + optional: [] + headers: + - key: Content-Type + value: application/json + request_body: + jsonrpc: '2.0' + method: eth_call + params: + - to: '{contractAddress}' # from input parameter + data: '{functionSelector}' # from input parameter + - latest + id: 1 + timeout_ms: 10000 + rate_limit: + requests_per_second: 0 + burst_size: 0 + endpoint_tested: true + sample_response: | + { + "jsonrpc": "2.0", + "id": 1, + "result": "0x0000000000000000000000000000000000000000000000000000000000000121" + } + response_mapping: + result_field: result + result_type: uint256 + conversion: hex_to_decimal + data_freshness: + timestamp_field: '' + max_age_seconds: 0 + +external_adapter_specifications: + environment_variables: + - name: ETHEREUM_RPC_URL + description: Ethereum JSON-RPC endpoint URL provided by the node operator + required: true + example: https://mainnet.infura.io/v3/YOUR_PROJECT_ID + endpoints: + - name: round + description: Fetches the round value from the XUSD contract + input_parameters: + - name: contractAddress + type: string + description: The Ethereum contract address to call + required: true + example: '0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94' + - name: functionSelector + type: string + description: The 4-byte function selector for the contract function to call + required: true + example: '0x146ca531' + contract_function: + name: round + signature: 'function round() public view returns (uint256)' + default_selector: '0x146ca531' + endpoint_request_response_examples: + - endpoint: round + request: | + { + "data": { + "endpoint": "round", + "contractAddress": "0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94", + "functionSelector": "0x146ca531" + } + } + response: | + { + "statusCode": 200, + "result": 289 + } + curl_example: | + curl -s -X POST $ETHEREUM_RPC_URL \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{ + "to": "0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94", + "data": "0x146ca531" + }, "latest"], + "id": 1 + }' + +data_source: + provider_name: Node Operator Ethereum RPC + type: on_chain_contract + api_documentation_url: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call + api_base_documentation: 'Ethereum JSON-RPC eth_call method' + contract_source: https://etherscan.io/address/0xE2Fc85BfB48C4cF147921fBE110cf92Ef9f26F94#code + support_contact: '' + +metadata: + business_owner: '[Nora McGlynn](/people/6201d0845d18ad007298fbf0)' + technical_owner: Wilson Chen + jira_ticket: OPDATA-4790 + deployment_chains: + - ethereum-mainnet diff --git a/ea-agent/examples/yaml-spec-template.yaml.template b/ea-agent/examples/yaml-spec-template.yaml.template new file mode 100644 index 0000000000..fe464199ee --- /dev/null +++ b/ea-agent/examples/yaml-spec-template.yaml.template @@ -0,0 +1,180 @@ +# REQUIRED: Basic adapter information +adapter_name: {{adapter_name}} # e.g. asseto-finance +adapter_type: {{adapter_type}} # e.g. source +description: {{description}} # e.g. External Adapter for querying NAV and Reserves (PoR) endpoints for Asseto tokenized funds (AoABT, CASH+, AoMMF) + +# REQUIRED: API protocol configuration +data_source_base_url: + base_url: {{base_url}} # e.g. https://provider.example.com/api + +# REQUIRED: Authentication configurations +# Choose one of the following authentication types: oauth | bearer_token | basic | api_key | none and remove the other authentication types +# And add the appropriate credentials for the chosen authentication type using provided fileds or modify the fields as needed +data_source_authentication: + type: {{auth_type}} # e.g. oauth | bearer_token | basic | api_key | none + + # -------- OAuth (Client Credentials or similar) -------- + credentials: + oauth_endpoint: {{oauth_endpoint}} # e.g. https://provider.example.com/api/oauth/token + token_request: + method: {{oauth_method}} # e.g. POST + content_type: {{oauth_content_type}} # e.g. application/json + body_parameters: + clientId: {{client_id}} # e.g. chainlink_18 + clientSecret: "{{client_secret_env}}" # e.g. {CLIENT_SECRET} + grant_type: {{grant_type}} # e.g. client_credentials + token_response: + access_token_field: {{access_token_field}} # e.g. access_token + token_usage: + header_name: {{oauth_header_name}} # e.g. Authorization + header_format: {{oauth_header_format}} # e.g. "Bearer {access_token}" + background_execute_ms: {{background_execute_ms}} # e.g. 10000 + token_expiration_seconds: {{token_expiration_seconds}} # e.g. 604800 + + # -------- Bearer Token -------- + credentials: + token: "{{bearer_token_env}}" # e.g. {API_TOKEN} + token_usage: + header_name: Authorization + header_format: "Bearer {token}" + + # -------- HTTP Basic -------- + credentials: + username: "{{basic_username_env}}" # e.g. {API_USER} + password: "{{basic_password_env}}" # e.g. {API_PASSWORD} + token_usage: + header_name: Authorization + header_format: "Basic {base64(username:password)}" + + # -------- API Key (Header or Query) -------- + credentials: + key: "{{api_key_env}}" # e.g. {API_KEY} + type: "{{api_key_type}}" # "header" | "query" + name: "{{api_key_name}}" # e.g. X-API-Key or api_key or key or other + +# REQUIRED: Endpoints definitions, please replace with the actual endpoints definitions and add as many endpoints as needed +data_source_endpoints: + - name: {{endpoint_name}} # e.g. reserves + method: {{HTTP_METHOD}} # e.g. GET + path: {{endpoint_path}} # e.g. /api/funds/{fundId}/reserves + description: {{description}} # e.g. Query total AUM (Proof of Reserves) for a specific fund + parameters: + required: {{required_params}} # e.g. [fundId] + optional: {{optional_params}} # e.g. [startDate, endDate] + headers: + - key: accept + value: application/json + - key: {{auth_header_key}} # e.g. Authorization + value: {{auth_header_value}} # e.g. Bearer {oauth_token} or {api_key} or {basic_token} + timeout_ms: {{timeout_ms}} # e.g. 10000 + rate_limit: + requests_per_second: {{rps}} # e.g. 10 + burst_size: {{burst_size}} # e.g. 20 + # REQUIRED: Sample response from the DP endpoint, following is an example response, please replace with the actual response + endpoint_tested: true | false + sample_response: | + { + "code": 0, + "message": "success", + "data": { + "list": [ + { + "fundId": 8, + "fundName": "CashPlus_BSC", + "netAssetValueDate": "2025-08-25", + "netAssetValue": "105.117", + "assetsUnderManagement": "1001180.979948", + "outstandingShares": "9524.444", + "netIncomeExpenses": "0" + }, + { + "fundId": 8, + "fundName": "CashPlus_BSC", + "netAssetValueDate": "2024-08-09", + "netAssetValue": "100", + "assetsUnderManagement": "0", + "outstandingShares": "0", + "netIncomeExpenses": "0" + } + ] + }, + "timestamp": 1757321913, + "traceID": "9421df2ff842631820b6dc38c009f7b6" + } + + # OPTIONAL: Mapping response fields from the DP endpoint to the EA output fields + # keys are the EA output fields, values are the DP endpoint response fields + # in the below example, EA output field result takes the value from the DP endpoint response field data.totalAum + response_mapping: # following is an example mapping, please replace with the actual mapping + result_field: data.totalAum + ripcord_field: data.ripcord + timestamp_field: data.updatedAt + + # Data freshness validation + data_freshness: + timestamp_field: {{timestamp_field}} # e.g. data.updatedAt + max_age_seconds: {{max_age_seconds}} # e.g. 86400 + +# REQUIRED: External Adapter Specifications for the EA, please replace with the actual specifications and add as many endpoints as needed +# This section defines the specifications for the EA to be built +external_adapter_specifications: + # REQUIRED: Environment variables for the EA, add or modify as needed + environment_variables: + - API_ENDPOINT: API_ENDPOINT + - CLIENT_ID: CLIENT_ID + - CLIENT_SECRET: CLIENT_SECRET + - GRANT_TYPE: GRANT_TYPE + - BACKGROUND_EXECUTE_MS: BACKGROUND_EXECUTE_MS + # REQUIRED: Endpoints for the EA, add more endpoints or modify as needed + endpoints: + - name: endpoint_name # e.g. nav | reserves | price | lwba | other + description: endpoint_description # e.g. net asset value of the fund + input_parameters: + - name: param_name # e.g. fundId | symbol | other + type: number | string | boolean | other + description: param_description # e.g. The fund id of the NAV to query + required: true | false + default: null + # REQUIRED: Request and response schemas for the EA endpoints above + endpoint_request_response_examples: + - endpoint: endpoint_name # e.g. nav | reserves | price | lwba | other + # following is an example of request and response, please replace with the actual request and response + request: | + { + "data": { + "endpoint": "reserves", + "fundId": 8 + } + } + response: | + { + "statusCode": 200, + "result": 0, + "data": { + "fundId": 8, + "fundName": "CashPlus_BSC", + "totalAUM": 0, + "totalDate": "2025-09-22 20:00:01", + "ripcord": false + }, + "timestamps": { + "providerDataRequestedUnixMs": 978347471111, + "providerDataReceivedUnixMs": 978347471111, + "providerIndicatedTimeUnixMs": 1758542882000 + } + } + +# Metadata for the Data Provider +data_source: + provider_name: {{provider_name}} + api_documentation_url: {{api_docs_url}} + api_base_documentation: {{api_base_docs_note}} + support_contact: {{support_contact}} + +# JIRA and Project Metadata for the EA +metadata: + business_owner: {{business_owner}} + technical_owner: {{technical_owner}} + jira_ticket: {{jira_ticket}} + deployment_chains: + - {{deployment_chain}} # e.g. arbitrum | bsc | ethereum-mainnet \ No newline at end of file diff --git a/ea-agent/pyproject.toml b/ea-agent/pyproject.toml new file mode 100644 index 0000000000..cbf4ded30c --- /dev/null +++ b/ea-agent/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "ea-scaffolding-agent" +version = "0.1.0" +description = "AI agent to scaffold the EA according to the yaml config input" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "claude-agent-sdk>=0.1.10", + "pydantic>=2.0.0", + "pytest>=8.4.2", + "pyyaml>=6.0", + "jsonschema>=4.0.0", + "specify-cli", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "pytest-asyncio>=0.21.0", +] + +[tool.uv.sources] +specify-cli = { git = "https://github.com/github/spec-kit.git" } diff --git a/ea-agent/requests/.gitkeep b/ea-agent/requests/.gitkeep new file mode 100644 index 0000000000..0d5d971741 --- /dev/null +++ b/ea-agent/requests/.gitkeep @@ -0,0 +1,3 @@ +# This directory contains EA request YAML files +# Files are created automatically by JIRA automation or manually by developers + diff --git a/ea-agent/scripts/setup-ea-env.sh b/ea-agent/scripts/setup-ea-env.sh new file mode 100755 index 0000000000..9ffce6a2a3 --- /dev/null +++ b/ea-agent/scripts/setup-ea-env.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Setup script for EA development environment +# Can be called from workflow or Python agent + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +cd "$PROJECT_ROOT" + +echo "📦 Installing project dependencies..." +yarn install + +echo "⚙️ Running yarn setup..." +yarn setup + +echo "🔓 Unplugging external adapter framework for agent exploration..." +yarn unplug @chainlink/external-adapter-framework + +echo "✅ EA environment setup complete!" + diff --git a/ea-agent/src/source_ea_agent.py b/ea-agent/src/source_ea_agent.py new file mode 100644 index 0000000000..3e15f1a161 --- /dev/null +++ b/ea-agent/src/source_ea_agent.py @@ -0,0 +1,516 @@ +""" +Source EA Agent - Orchestrates AI agents to build Source External Adapters. + +Environment Variables: + WORKFLOW_MODEL: Model to use (default: claude-opus-4-5@20251101) + ENVIRONMENT: Environment name (default: development) + VERBOSE_LOGGING: Log all messages (default: true) + JSON_LOG_PATH: Path for streaming JSON logs + SUMMARY_LOG_PATH: Path for final summary JSON +""" + +import asyncio +import json +import logging +import os +import sys +import uuid +from datetime import datetime, timezone +from typing import Any + +from pydantic import BaseModel, Field +from claude_agent_sdk import ClaudeAgentOptions, AssistantMessage, TextBlock, query + +# Optional SDK imports +try: + from claude_agent_sdk import ToolUseBlock, ToolResultBlock + HAS_TOOL_BLOCKS = True +except ImportError: + HAS_TOOL_BLOCKS = False + + +# ============================================================================= +# Logging +# ============================================================================= + +class Logger: + """Structured logger with console and optional JSON file output.""" + + def __init__(self, name: str = "workflow"): + self._logger = logging.getLogger(name) + self._logger.setLevel(logging.INFO) + self._logger.handlers.clear() + self._logger.addHandler(self._console_handler()) + + def _console_handler(self) -> logging.Handler: + handler = logging.StreamHandler() + handler.setFormatter(self._ConsoleFormatter()) + return handler + + def _json_handler(self, filepath: str) -> logging.Handler: + handler = logging.FileHandler(filepath) + handler.setFormatter(self._JsonFormatter()) + return handler + + def add_json_file(self, filepath: str): + self._logger.addHandler(self._json_handler(filepath)) + + def info(self, event: str, **data): + self._log(logging.INFO, event, data) + + def error(self, event: str, **data): + self._log(logging.ERROR, event, data) + + def _log(self, level: int, event: str, data: dict): + self._logger.log(level, event, extra={"data": {"event": event, **data}}) + + class _ConsoleFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + event = record.getMessage() + data = getattr(record, "data", {}) + parts = [f"{k}={json.dumps(v) if isinstance(v, dict) else v}" + for k, v in data.items() if k != "event"] + suffix = f" | {' '.join(parts)}" if parts else "" + return f"{ts} | {record.levelname} | {event}{suffix}" + + class _JsonFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + data = {"timestamp": datetime.now(timezone.utc).isoformat(), + "level": record.levelname, + "message": record.getMessage()} + data.update(getattr(record, "data", {})) + return json.dumps(data) + + +log = Logger("workflow") + + +# ============================================================================= +# Pydantic Models +# ============================================================================= + +class InitializationResult(BaseModel): + ea_package_path: str = Field(description="The path to the EA package.") + + +class ValidationResult(BaseModel): + approved: bool = Field(description="Whether validation passed.") + rationale: str = Field(default="", description="Feedback if not approved.") + + +# ============================================================================= +# Utilities +# ============================================================================= + +def read_file(path: str) -> str: + """Read file contents or exit on error.""" + try: + with open(path) as f: + return f.read() + except FileNotFoundError: + log.error("file_not_found", path=path) + sys.exit(1) + + +def serialize_message(message: Any) -> dict: + """Convert SDK message to JSON-serializable dict.""" + result = {"type": type(message).__name__, + "timestamp": datetime.now(timezone.utc).isoformat()} + + if isinstance(message, AssistantMessage): + result["content"] = [serialize_block(b) for b in message.content] + elif hasattr(message, "subtype"): + result["subtype"] = message.subtype + elif hasattr(message, "__dict__"): + for k, v in message.__dict__.items(): + if not k.startswith("_"): + try: + json.dumps(v) + result[k] = v + except (TypeError, ValueError): + result[k] = str(v) + return result + + +def serialize_block(block: Any) -> dict: + """Convert SDK content block to JSON-serializable dict.""" + result = {"type": type(block).__name__} + + if isinstance(block, TextBlock): + result["text"] = block.text + elif HAS_TOOL_BLOCKS and isinstance(block, ToolUseBlock): + result.update(name=block.name, id=block.id, input=getattr(block, "input", {})) + elif HAS_TOOL_BLOCKS and isinstance(block, ToolResultBlock): + result.update(tool_use_id=block.tool_use_id, + content=str(getattr(block, "content", ""))[:1000]) + elif hasattr(block, "__dict__"): + for k, v in block.__dict__.items(): + if not k.startswith("_"): + try: + json.dumps(v) + result[k] = v + except (TypeError, ValueError): + result[k] = str(v)[:1000] + return result + + +# ============================================================================= +# Agent Runner +# ============================================================================= + +async def run_agent( + name: str, + system_prompt_path: str, + prompt: str, + trace_id: str, + phase: str, + iteration: int = 1, + model: str = "claude-opus-4-5@20251101", + options: dict = None, + output_schema: dict = None, +) -> tuple[Any, dict]: + """Run an agent and return (result, run_log).""" + + started_at = datetime.now(timezone.utc).isoformat() + log.info("agent_start", trace_id=trace_id, agent=name, phase=phase, iteration=iteration) + + # Build agent options + system_prompt = read_file(system_prompt_path) + system_prompt += "\n\nCRITICAL: DO NOT create markdown docs. Only modify code/config files within the package directory. KISS principle." + + output_format = {"type": "json_schema", "schema": output_schema} if output_schema else None + if output_format: + log.info("output_format_configured", trace_id=trace_id, agent=name) + + agent_opts = ClaudeAgentOptions( + model=model, + system_prompt=system_prompt, + cwd=os.getcwd(), + output_format=output_format, + **(options or {}) + ) + + # Run agent + text = "" + structured = None + messages = [] + tokens_in = tokens_out = 0 + success = True + error = None + verbose = os.environ.get("VERBOSE_LOGGING", "true").lower() == "true" + + try: + async for msg in query(prompt=prompt, options=agent_opts): + # Log messages + if verbose: + serialized = serialize_message(msg) + messages.append(serialized) + log.info("message", trace_id=trace_id, agent=name, phase=phase, + iteration=iteration, message=serialized) + + # Accumulate text + if isinstance(msg, AssistantMessage): + for block in msg.content: + if isinstance(block, TextBlock): + text += block.text + + # Capture structured output + if hasattr(msg, "structured_output") and msg.structured_output: + structured = msg.structured_output + log.info("structured_output", trace_id=trace_id, agent=name, data=structured) + + # Handle result message + if getattr(msg, "type", None) == "result": + subtype = getattr(msg, "subtype", None) + log.info("result", trace_id=trace_id, agent=name, subtype=subtype) + if subtype == "error_max_structured_output_retries": + log.error("structured_output_failed", trace_id=trace_id, agent=name) + + # Capture usage + if hasattr(msg, "usage"): + tokens_in = getattr(msg.usage, "input_tokens", 0) + tokens_out = getattr(msg.usage, "output_tokens", 0) + + except Exception as e: + success = False + error = str(e) + log.error("agent_error", trace_id=trace_id, agent=name, phase=phase, + iteration=iteration, error=error, error_type=type(e).__name__) + + log.info("agent_complete", trace_id=trace_id, agent=name, phase=phase, + iteration=iteration, success=success, tokens_in=tokens_in, tokens_out=tokens_out) + + return (structured or text, { + "agent": name, "phase": phase, "iteration": iteration, + "started_at": started_at, "ended_at": datetime.now(timezone.utc).isoformat(), + "success": success, "error": error, + "input_tokens": tokens_in, "output_tokens": tokens_out, "messages": messages + }) + + +# ============================================================================= +# Execute-Validate Loop +# ============================================================================= + +async def run_execute_validate_loop( + executor_name: str, + executor_prompt_path: str, + executor_prompt: str, + validator_name: str, + validator_prompt_path: str, + validator_prompt: str, + phase: str, + trace_id: str, + model: str, + executor_opts: dict, + validator_opts: dict, + agent_runs: list, + max_iterations: int = 3, + required_approvals: int = 1, + feedback_to_prompt: callable = None, +) -> tuple[int, int]: + """ + Generic execute-validate loop. + + Returns (iterations, approvals). + + Args: + executor_*: Agent that executes/fixes (writer, developer) + validator_*: Agent that validates/reviews + feedback_to_prompt: Function(feedback, original_prompt) -> new_prompt for retry + """ + + approvals = 0 + feedback = "" + iteration = 1 + + # Default feedback handler: append to original prompt + if feedback_to_prompt is None: + feedback_to_prompt = lambda fb, orig: f"{orig}\n\nPrevious validation feedback:\n{fb}" + + while approvals < required_approvals and iteration <= max_iterations: + # Execute + current_prompt = executor_prompt if not feedback else feedback_to_prompt(feedback, executor_prompt) + + _, run_log = await run_agent( + name=executor_name, + system_prompt_path=executor_prompt_path, + prompt=current_prompt, + trace_id=trace_id, + phase=phase, + iteration=iteration, + model=model, + options=executor_opts, + ) + agent_runs.append(run_log) + + # Validate (run required_approvals rounds, all must pass) + for round_num in range(1, required_approvals + 1): + round_prompt = validator_prompt + if required_approvals > 1: + round_prompt = f"{validator_prompt} (Round {round_num}/{required_approvals})" + + result, run_log = await run_agent( + name=validator_name, + system_prompt_path=validator_prompt_path, + prompt=round_prompt, + trace_id=trace_id, + phase=phase, + iteration=iteration, + model=model, + options=validator_opts, + output_schema=ValidationResult.model_json_schema(), + ) + agent_runs.append(run_log) + + # Process validation result + if isinstance(result, dict): + validation = ValidationResult.model_validate(result) + log.info("validation", trace_id=trace_id, phase=phase, iteration=iteration, + round=round_num, approved=validation.approved, + rationale=validation.rationale[:500] if validation.rationale else None) + + if validation.approved: + approvals += 1 + if approvals >= required_approvals: + break + else: + approvals = 0 + feedback = validation.rationale + iteration += 1 + break + else: + log.info("validation", trace_id=trace_id, phase=phase, iteration=iteration, + round=round_num, approved=False, rationale="No structured output") + approvals = 0 + feedback = str(result) + iteration += 1 + break + + return iteration, approvals + + +# ============================================================================= +# Main Workflow +# ============================================================================= + +async def main(): + # Setup + json_log = os.environ.get("JSON_LOG_PATH") + if json_log: + log.add_json_file(json_log) + + if len(sys.argv) < 2: + log.error("startup_error", error="Missing requirements file", + usage="python ea_agent.py ") + sys.exit(1) + + req_file = sys.argv[1] + requirements = read_file(req_file) + + # Workflow state + trace_id = str(uuid.uuid4())[:8] + started_at = datetime.now(timezone.utc).isoformat() + model = os.environ.get("WORKFLOW_MODEL", "claude-opus-4-5@20251101") + environment = os.environ.get("ENVIRONMENT", "development") + agent_runs = [] + ea_path = None + success = False + error_msg = None + + log.info("workflow_start", trace_id=trace_id, requirements_file=req_file, + model=model, environment=environment) + + edit_opts = {"permission_mode": "acceptEdits", + "allowed_tools": ["Read", "Write", "Bash", "List", "GlobFileSearch"]} + validator_opts = {"permission_mode": "acceptEdits", + "allowed_tools": ["Read", "Bash", "List", "GlobFileSearch"]} + + try: + # Phase 1: Initialize EA + log.info("phase_start", trace_id=trace_id, phase="initialization") + + result, run_log = await run_agent( + name="EA Developer", + system_prompt_path=".claude/agents/ea_developer.md", + prompt=f"Initialize the EA project.\n\nRequirements:\n{requirements}", + trace_id=trace_id, + phase="initialization", + model=model, + options=edit_opts, + output_schema=InitializationResult.model_json_schema(), + ) + agent_runs.append(run_log) + + if not isinstance(result, dict): + raise ValueError(f"EA Developer did not return structured output: {str(result)[-500:]}") + + ea_path = InitializationResult.model_validate(result).ea_package_path + log.info("phase_complete", trace_id=trace_id, phase="initialization", ea_path=ea_path) + + read_only_opts = {"permission_mode": "acceptEdits", + "allowed_tools": ["Read", "List", "GlobFileSearch"]} + + # Phase 2: Code Review + log.info("phase_start", trace_id=trace_id, phase="code_review") + iters, approvals = await run_execute_validate_loop( + executor_name="EA Developer", + executor_prompt_path=".claude/agents/ea_developer.md", + executor_prompt=f"Review and fix any code quality issues in {ea_path}.", + validator_name="Code Reviewer", + validator_prompt_path=".claude/agents/ea_code_reviewer.md", + validator_prompt=f"Review the EA code at: {ea_path}\n\nOriginal Requirements:\n{requirements}", + phase="code_review", + trace_id=trace_id, + model=model, + executor_opts=edit_opts, + validator_opts=read_only_opts, + agent_runs=agent_runs, + ) + log.info("phase_complete", trace_id=trace_id, phase="code_review", + iterations=iters, approvals=approvals) + + # Phase 3: Integration Tests + log.info("phase_start", trace_id=trace_id, phase="integration_testing") + iters, approvals = await run_execute_validate_loop( + executor_name="Integration Test Writer", + executor_prompt_path=".claude/agents/ea_integration_test_writer.md", + executor_prompt=f"Generate integration tests for: {ea_path}.", + validator_name="Integration Test Validator", + validator_prompt_path=".claude/agents/ea_integration_test_validator.md", + validator_prompt=f"Validate integration tests for: {ea_path}. Run if possible.", + phase="integration_testing", + trace_id=trace_id, + model=model, + executor_opts=edit_opts, + validator_opts=validator_opts, + agent_runs=agent_runs, + required_approvals=3, + ) + log.info("phase_complete", trace_id=trace_id, phase="integration_testing", + iterations=iters, approvals=approvals) + + # Phase 4: Unit Tests + log.info("phase_start", trace_id=trace_id, phase="unit_testing") + iters, approvals = await run_execute_validate_loop( + executor_name="Unit Test Writer", + executor_prompt_path=".claude/agents/ea_unit_test_writer.md", + executor_prompt=f"Generate unit tests for: {ea_path}.", + validator_name="Unit Test Validator", + validator_prompt_path=".claude/agents/ea_unit_test_validator.md", + validator_prompt=f"Validate unit tests for: {ea_path}. Run if possible.", + phase="unit_testing", + trace_id=trace_id, + model=model, + executor_opts=edit_opts, + validator_opts=validator_opts, + agent_runs=agent_runs, + required_approvals=3, + ) + log.info("phase_complete", trace_id=trace_id, phase="unit_testing", + iterations=iters, approvals=approvals) + + success = True + + except Exception as e: + error_msg = str(e) + log.error("workflow_error", trace_id=trace_id, error=error_msg, + error_type=type(e).__name__) + raise + + finally: + # Summary + ended_at = datetime.now(timezone.utc).isoformat() + tokens_in = sum(r.get("input_tokens", 0) for r in agent_runs) + tokens_out = sum(r.get("output_tokens", 0) for r in agent_runs) + + summary = { + "trace_id": trace_id, + "started_at": started_at, + "ended_at": ended_at, + "success": success, + "error": error_msg, + "requirements_file": req_file, + "ea_package_path": ea_path, + "model": model, + "environment": environment, + "total_input_tokens": tokens_in, + "total_output_tokens": tokens_out, + "agent_runs": agent_runs, + } + + log.info("workflow_complete", trace_id=trace_id, success=success, + ea_path=ea_path, tokens_in=tokens_in, tokens_out=tokens_out, + agents=len(agent_runs)) + + # Write summary if requested + summary_path = os.environ.get("SUMMARY_LOG_PATH") + if summary_path: + with open(summary_path, "w") as f: + json.dump(summary, f, indent=2) + log.info("summary_written", trace_id=trace_id, path=summary_path) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/ea-agent/uv.lock b/ea-agent/uv.lock new file mode 100644 index 0000000000..d4cc7c67b7 --- /dev/null +++ b/ea-agent/uv.lock @@ -0,0 +1,940 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "claude-agent-sdk" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "mcp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/13/a6c08fd20f12dcc685c852221c7159cb902580cf5f626b59be6c942f4644/claude_agent_sdk-0.1.10.tar.gz", hash = "sha256:fda9a520c9904f0d00a15c36e2530ac174a6c29a4c4555ee4c8ec8f35af5fd08", size = 51302, upload-time = "2025-11-25T16:01:14.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/08/710efbe4a4090bcb6e7aff16e5fa03867b19bfa9bc1814973da5d4e5cc5d/claude_agent_sdk-0.1.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2be5c3da0cf9959db817d7ff01ab87d94f68c5625efc7c11c7af94abcdd11b83", size = 49449922, upload-time = "2025-11-25T16:01:01.787Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f0/6ec298fa1921551c6a8e4d4bc2caf5f75728219a4b58e537985d36717049/claude_agent_sdk-0.1.10-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:f2576d7429753c3b8081a4c377855940b95a3359552fadbedef7f0fcdc01d8ca", size = 65319866, upload-time = "2025-11-25T16:01:05.697Z" }, + { url = "https://files.pythonhosted.org/packages/83/30/11d101012e5b02361bd645e590498894a7bf30586a2b4626500e5157c872/claude_agent_sdk-0.1.10-py3-none-win_amd64.whl", hash = "sha256:a76567eb07143ad49617e30a41e41756a2542ff9986b83ec77b0d54ba2fa5cfa", size = 68211967, upload-time = "2025-11-25T16:01:10.526Z" }, +] + +[[package]] +name = "click" +version = "8.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "ea-scaffolding-agent" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "claude-agent-sdk" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pytest" }, + { name = "pyyaml" }, + { name = "specify-cli" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "claude-agent-sdk", specifier = ">=0.1.10" }, + { name = "jsonschema", specifier = ">=4.0.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "specify-cli", git = "https://github.com/github/spec-kit.git" }, +] +provides-extras = ["dev"] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +socks = [ + { name = "socksio" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/7a/280d644f906f077e4f4a6d327e9b6e5a936624395ad1bf6ee9165a9d9959/httpx_sse-0.4.2.tar.gz", hash = "sha256:5bb6a2771a51e6c7a5f5c645e40b8a5f57d8de708f46cb5f3868043c3c18124e", size = 16000, upload-time = "2025-10-07T08:10:05.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/e5/ec31165492ecc52426370b9005e0637d6da02f9579283298affcb1ab614d/httpx_sse-0.4.2-py3-none-any.whl", hash = "sha256:a9fa4afacb293fa50ef9bacb6cae8287ba5fd1f4b1c2d10a35bb981c41da31ab", size = 9018, upload-time = "2025-10-07T08:10:04.257Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918, upload-time = "2025-10-02T16:58:20.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266, upload-time = "2025-10-02T16:58:19.039Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/a9/ec440f02e57beabdfd804725ef1e38ac1ba00c49854d298447562e119513/pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1", size = 2111456, upload-time = "2025-10-06T21:10:09.824Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f9/6bc15bacfd8dcfc073a1820a564516d9c12a435a9a332d4cbbfd48828ddd/pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176", size = 1915012, upload-time = "2025-10-06T21:10:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/38/8a/d9edcdcdfe80bade17bed424284427c08bea892aaec11438fa52eaeaf79c/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0", size = 1973762, upload-time = "2025-10-06T21:10:13.154Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b3/ff225c6d49fba4279de04677c1c876fc3dc6562fd0c53e9bfd66f58c51a8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575", size = 2065386, upload-time = "2025-10-06T21:10:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/47/ba/183e8c0be4321314af3fd1ae6bfc7eafdd7a49bdea5da81c56044a207316/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20", size = 2252317, upload-time = "2025-10-06T21:10:15.719Z" }, + { url = "https://files.pythonhosted.org/packages/57/c5/aab61e94fd02f45c65f1f8c9ec38bb3b33fbf001a1837c74870e97462572/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4", size = 2373405, upload-time = "2025-10-06T21:10:17.017Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4f/3aaa3bd1ea420a15acc42d7d3ccb3b0bbc5444ae2f9dbc1959f8173e16b8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d", size = 2073794, upload-time = "2025-10-06T21:10:18.383Z" }, + { url = "https://files.pythonhosted.org/packages/58/bd/e3975cdebe03ec080ef881648de316c73f2a6be95c14fc4efb2f7bdd0d41/pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1", size = 2194430, upload-time = "2025-10-06T21:10:19.638Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/6b7e7217f147d3b3105b57fb1caec3c4f667581affdfaab6d1d277e1f749/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9", size = 2154611, upload-time = "2025-10-06T21:10:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/239c2fe76bd8b7eef9ae2140d737368a3c6fea4fd27f8f6b4cde6baa3ce9/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1", size = 2329809, upload-time = "2025-10-06T21:10:22.678Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/77a821a67ff0786f2f14856d6bd1348992f695ee90136a145d7a445c1ff6/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea", size = 2327907, upload-time = "2025-10-06T21:10:24.447Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/b54512bb9df7f64c586b369328c30481229b70ca6a5fcbb90b715e15facf/pydantic_core-2.41.1-cp311-cp311-win32.whl", hash = "sha256:2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f", size = 1989964, upload-time = "2025-10-06T21:10:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/9d/72/63c9a4f1a5c950e65dd522d7dd67f167681f9d4f6ece3b80085a0329f08f/pydantic_core-2.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298", size = 2025158, upload-time = "2025-10-06T21:10:27.522Z" }, + { url = "https://files.pythonhosted.org/packages/d8/16/4e2706184209f61b50c231529257c12eb6bd9eb36e99ea1272e4815d2200/pydantic_core-2.41.1-cp311-cp311-win_arm64.whl", hash = "sha256:7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5", size = 1972297, upload-time = "2025-10-06T21:10:28.814Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4", size = 2099300, upload-time = "2025-10-06T21:10:30.463Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601", size = 1910179, upload-time = "2025-10-06T21:10:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00", size = 1957225, upload-time = "2025-10-06T21:10:33.11Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741", size = 2053315, upload-time = "2025-10-06T21:10:34.87Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8", size = 2224298, upload-time = "2025-10-06T21:10:36.233Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51", size = 2351797, upload-time = "2025-10-06T21:10:37.601Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5", size = 2074921, upload-time = "2025-10-06T21:10:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115", size = 2187767, upload-time = "2025-10-06T21:10:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d", size = 2136062, upload-time = "2025-10-06T21:10:42.09Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5", size = 2317819, upload-time = "2025-10-06T21:10:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513", size = 2312267, upload-time = "2025-10-06T21:10:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479", size = 1990927, upload-time = "2025-10-06T21:10:46.738Z" }, + { url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50", size = 2034703, upload-time = "2025-10-06T21:10:48.524Z" }, + { url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde", size = 1972719, upload-time = "2025-10-06T21:10:50.256Z" }, + { url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf", size = 2105825, upload-time = "2025-10-06T21:10:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb", size = 1910126, upload-time = "2025-10-06T21:10:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669", size = 1961472, upload-time = "2025-10-06T21:10:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f", size = 2063230, upload-time = "2025-10-06T21:10:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4", size = 2229469, upload-time = "2025-10-06T21:10:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62", size = 2347986, upload-time = "2025-10-06T21:11:00.847Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014", size = 2072216, upload-time = "2025-10-06T21:11:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d", size = 2193047, upload-time = "2025-10-06T21:11:03.787Z" }, + { url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f", size = 2140613, upload-time = "2025-10-06T21:11:05.607Z" }, + { url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257", size = 2327641, upload-time = "2025-10-06T21:11:07.143Z" }, + { url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32", size = 2318229, upload-time = "2025-10-06T21:11:08.73Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d", size = 1997911, upload-time = "2025-10-06T21:11:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b", size = 2034301, upload-time = "2025-10-06T21:11:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb", size = 1977238, upload-time = "2025-10-06T21:11:14.1Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc", size = 1875626, upload-time = "2025-10-06T21:11:15.69Z" }, + { url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67", size = 2045708, upload-time = "2025-10-06T21:11:17.258Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795", size = 1997171, upload-time = "2025-10-06T21:11:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b", size = 2107836, upload-time = "2025-10-06T21:11:20.432Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a", size = 1904449, upload-time = "2025-10-06T21:11:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674", size = 1961750, upload-time = "2025-10-06T21:11:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4", size = 2063305, upload-time = "2025-10-06T21:11:26.556Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31", size = 2228959, upload-time = "2025-10-06T21:11:28.426Z" }, + { url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706", size = 2345421, upload-time = "2025-10-06T21:11:30.226Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b", size = 2065288, upload-time = "2025-10-06T21:11:32.019Z" }, + { url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be", size = 2189759, upload-time = "2025-10-06T21:11:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04", size = 2140747, upload-time = "2025-10-06T21:11:35.781Z" }, + { url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4", size = 2327416, upload-time = "2025-10-06T21:11:37.75Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8", size = 2318138, upload-time = "2025-10-06T21:11:39.463Z" }, + { url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl", hash = "sha256:a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159", size = 1998429, upload-time = "2025-10-06T21:11:41.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl", hash = "sha256:1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae", size = 2028870, upload-time = "2025-10-06T21:11:43.66Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9", size = 1974275, upload-time = "2025-10-06T21:11:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4", size = 1875124, upload-time = "2025-10-06T21:11:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e", size = 2043075, upload-time = "2025-10-06T21:11:49.542Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762", size = 1995341, upload-time = "2025-10-06T21:11:51.497Z" }, + { url = "https://files.pythonhosted.org/packages/16/89/d0afad37ba25f5801735af1472e650b86baad9fe807a42076508e4824a2a/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:68f2251559b8efa99041bb63571ec7cdd2d715ba74cc82b3bc9eff824ebc8bf0", size = 2124001, upload-time = "2025-10-07T10:49:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c4/08609134b34520568ddebb084d9ed0a2a3f5f52b45739e6e22cb3a7112eb/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:c7bc140c596097cb53b30546ca257dbe3f19282283190b1b5142928e5d5d3a20", size = 1941841, upload-time = "2025-10-07T10:49:56.248Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/94a4877094e5fe19a3f37e7e817772263e2c573c94f1e3fa2b1eee56ef3b/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2896510fce8f4725ec518f8b9d7f015a00db249d2fd40788f442af303480063d", size = 1961129, upload-time = "2025-10-07T10:49:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/a2/30/23a224d7e25260eb5f69783a63667453037e07eb91ff0e62dabaadd47128/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ced20e62cfa0f496ba68fa5d6c7ee71114ea67e2a5da3114d6450d7f4683572a", size = 2148770, upload-time = "2025-10-07T10:49:59.959Z" }, + { url = "https://files.pythonhosted.org/packages/2b/3e/a51c5f5d37b9288ba30683d6e96f10fa8f1defad1623ff09f1020973b577/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06", size = 2115344, upload-time = "2025-10-07T10:50:02.466Z" }, + { url = "https://files.pythonhosted.org/packages/5a/bd/389504c9e0600ef4502cd5238396b527afe6ef8981a6a15cd1814fc7b434/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb", size = 1927994, upload-time = "2025-10-07T10:50:04.379Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9c/5111c6b128861cb792a4c082677e90dac4f2e090bb2e2fe06aa5b2d39027/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca", size = 1959394, upload-time = "2025-10-07T10:50:06.335Z" }, + { url = "https://files.pythonhosted.org/packages/14/3f/cfec8b9a0c48ce5d64409ec5e1903cb0b7363da38f14b41de2fcb3712700/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28", size = 2147365, upload-time = "2025-10-07T10:50:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6c/fa3e45c2b054a1e627a89a364917f12cbe3abc3e91b9004edaae16e7b3c5/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1", size = 2112094, upload-time = "2025-10-07T10:50:25.513Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/7eebc38b4658cc8e6902d0befc26388e4c2a5f2e179c561eeb43e1922c7b/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696", size = 1935300, upload-time = "2025-10-07T10:50:27.715Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/9fe640194a1717a464ab861d43595c268830f98cb1e2705aa134b3544b70/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222", size = 1970417, upload-time = "2025-10-07T10:50:29.573Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/f4cdfaf483b78ee65362363e73b6b40c48e067078d7b146e8816d5945ad6/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2", size = 2190745, upload-time = "2025-10-07T10:50:31.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c1/18f416d40a10f44e9387497ba449f40fdb1478c61ba05c4b6bdb82300362/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506", size = 2150888, upload-time = "2025-10-07T10:50:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/42/30/134c8a921630d8a88d6f905a562495a6421e959a23c19b0f49b660801d67/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656", size = 2324489, upload-time = "2025-10-07T10:50:36.48Z" }, + { url = "https://files.pythonhosted.org/packages/9c/48/a9263aeaebdec81e941198525b43edb3b44f27cfa4cb8005b8d3eb8dec72/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e", size = 2322763, upload-time = "2025-10-07T10:50:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb", size = 2144046, upload-time = "2025-10-07T10:50:40.686Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "readchar" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/f8/8657b8cbb4ebeabfbdf991ac40eca8a1d1bd012011bd44ad1ed10f5cb494/readchar-4.2.1.tar.gz", hash = "sha256:91ce3faf07688de14d800592951e5575e9c7a3213738ed01d394dcc949b79adb", size = 9685, upload-time = "2024-11-04T18:28:07.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/10/e4b1e0e5b6b6745c8098c275b69bc9d73e9542d5c7da4f137542b499ed44/readchar-4.2.1-py3-none-any.whl", hash = "sha256:a769305cd3994bb5fa2764aa4073452dc105a4ec39068ffe6efd3c20c60acc77", size = 9350, upload-time = "2024-11-04T18:28:02.859Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" }, + { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" }, + { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, + { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, + { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, + { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, + { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, + { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, + { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, + { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, + { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, + { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, + { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, + { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, + { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, + { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, + { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" }, + { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, +] + +[[package]] +name = "specify-cli" +version = "0.0.19" +source = { git = "https://github.com/github/spec-kit.git#e65660ffc3ce5c1ac83451dad041e29ef2b46ad0" } +dependencies = [ + { name = "httpx", extra = ["socks"] }, + { name = "platformdirs" }, + { name = "readchar" }, + { name = "rich" }, + { name = "truststore" }, + { name = "typer" }, +] + +[[package]] +name = "sse-starlette" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, +]