diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..1feda78 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,57 @@ +name: docs + +on: + pull_request: + push: + branches: + - main + - master + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: docs + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Set up uv + uses: astral-sh/setup-uv@v4 + + - name: Install docs dependencies + run: uv sync --extra docs + + - name: Build docs + run: uv run mkdocs build --strict + + - name: Upload Pages artifact + if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 8f95aaf..42d13ad 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,35 @@ A small, typed framework to evaluate pipeline outputs for single-feature, single-entity, and multi-entity extraction tasks against gold data. It aligns predictions to gold, normalizes values (text/number/date/category), computes macro metrics per feature, aggregates totals, and writes timestamped logs. +## Why this library exists + +This library is built for evaluation-driven development (EDD): the idea that extraction and classification workflows should be improved through a repeatable loop of gold-set design, implementation, evaluation, error review, and iteration. + +In many real projects, building the first workflow is not the hardest part. The harder part is setting up the evaluation layer around it quickly enough that the team actually keeps the discipline to measure performance, inspect mistakes, and improve behavior over time. That problem becomes even sharper when the workflow depends on domain knowledge the developer does not have directly, but a domain expert can provide in limited bursts. + +The typical loop looks like this: + +1. Define a representative gold test set with a domain expert. +2. Build a baseline workflow or pipeline. +3. Compare predictions against the gold data, calculate metrics, and visualize the results. +4. Review grouped mistakes, refine the workflow, and repeat. + +This library is intended to standardize step 3. Instead of rebuilding comparison logic, metrics code, logging, and visualization for every project, you can focus on: + +1. defining the test set correctly +2. building the workflow itself +3. using this library to evaluate predictions against the gold set + +That lowers the cost of doing EDD and makes it easier to balance speed and quality in production-oriented development. + +Example task shapes include: + +- one label per text, such as sentiment or routing class: `SINGLE_FEATURE` +- one structured object per record, such as key fields from an article: `SINGLE_ENTITY` +- multiple entities per source, such as contracts extracted from a report: `MULTI_ENTITY` + +The most important design work still lives in the test set. That is where intended behavior, definitions, and useful biases are encoded for the task. + ## Install Using `uv`: diff --git a/docs/concepts/alignment.md b/docs/concepts/alignment.md new file mode 100644 index 0000000..c185865 --- /dev/null +++ b/docs/concepts/alignment.md @@ -0,0 +1,98 @@ +# Alignment + +Alignment decides which predicted rows are compared against which gold rows. The library has two alignment strategies: indexed matching and entity matching. + +## `IndexAligner` + +`IndexAligner` is used for `SINGLE_FEATURE` and `SINGLE_ENTITY`. + +Current behavior: + +- reads `run_config.index_key_name` +- builds a map from each gold key value to one gold row index +- iterates through predicted rows and matches rows with the same key +- returns matched pairs with similarity score `1.0` +- also returns unmatched predicted indices and unmatched gold indices + +### Important implications + +- matching is exact equality on the key field +- duplicate key values in the gold DataFrame are ambiguous and should be avoided +- unmatched rows are not included in per-feature scoring + +## `EntityAligner` + +`EntityAligner` is used for `MULTI_ENTITY`. + +Current behavior: + +1. build candidate predicted/gold pairs +2. compute similarity for each pair +3. drop pairs below `minimum_similarity_threshold` +4. optionally keep only the top `maximum_candidate_pairs` +5. shuffle candidates using a fixed seed +6. stable-sort by similarity descending +7. greedily choose non-overlapping pairs + +The result is deterministic for a fixed seed and input order. + +## Matching modes + +### Exact matching + +When `matching_mode="exact"`: + +- the aligner checks only features where `is_mandatory_for_matching=True` +- if any mandatory feature is unequal, pair similarity is `0.0` +- otherwise pair similarity is `1.0` + +This is strict matching. There is no partial credit. + +### Weighted matching + +When `matching_mode="weighted"`: + +- every feature contributes a similarity score +- the final score is a weighted average +- weights come from `FeatureRule.weight_for_matching` + +Feature-level similarity rules: + +- `text`: token-set overlap after normalization +- `category`: `1.0` if equal, else `0.0` +- `number`: `1.0` if equal under tolerance rules, else `0.0` +- `date`: `1.0` if equal under date tolerance rules, else `0.0` + +## Threshold behavior + +`minimum_similarity_threshold` applies after pair similarity is computed. + +- pairs below the threshold are ignored entirely +- pairs at or above the threshold remain candidates for greedy selection + +Practical effect: + +- a high threshold increases precision of entity matching +- a low threshold increases recall of candidate pairs but can create more ambiguous competition + +## Determinism and tie-breaking + +For weighted matching, the aligner: + +- seeds Python's `random` module with `random_tie_breaker_seed` +- shuffles candidate pairs +- then sorts by similarity descending using a stable sort + +This means equal-score pairs are resolved reproducibly for a given seed. + +## What alignment affects downstream + +- matched pairs feed the per-feature metrics and row accuracy calculations +- unmatched predicted and unmatched gold entities feed the multi-entity presence summary +- indexed tasks do not expose entity presence metrics because alignment is assumed to be explicit + +## Related pages + +- [Task Types](task-types.md) +- [Feature Rules](feature-rules.md) +- [Metrics](metrics.md) diff --git a/docs/concepts/evaluation-driven-development.md b/docs/concepts/evaluation-driven-development.md new file mode 100644 index 0000000..b881b50 --- /dev/null +++ b/docs/concepts/evaluation-driven-development.md @@ -0,0 +1,142 @@ +# Why This Library Exists + +`extraction-testing` is designed to support evaluation-driven development (EDD) for extraction and classification workflows. + +The short version is: + +- building a first workflow is often feasible +- building a disciplined evaluation layer around it is the part teams often postpone +- postponing evaluation makes iteration less reliable and performance harder to explain +- this library exists to standardize that evaluation layer + +## The practical problem + +Suppose you want to extract contracts from a monthly report. + +Implementing a first version of the workflow is usually straightforward enough: + +- parse the report +- run the extraction logic +- return structured predictions + +What slows teams down is the next step: turning that workflow into something they can improve systematically. + +To do that well, the team needs to: + +- compare predictions against a gold set +- compute useful metrics +- inspect mistakes in a structured way +- visualize and communicate overall performance + +That work is important, but it is often rewritten from scratch for each project. As a result, teams either delay it or keep the evaluation layer too ad hoc to drive disciplined iteration. + +## The EDD loop + +The usual evaluation-driven loop looks like this: + +```mermaid +flowchart LR + A["Domain expert defines gold set"] --> B["Developer builds baseline workflow"] + B --> C["Run workflow on evaluation set"] + C --> D["Compare predictions with gold data"] + D --> E["Compute metrics, logs, and charts"] + E --> F["Review grouped errors with domain expert"] + F --> G["Refine workflow or prompt"] + G --> C + E --> H["Share expected performance with stakeholders"] +``` + +This loop matters because it gives the team two different outputs: + +- an iteration loop for improving the workflow +- a performance summary that can be shared with stakeholders + +## Where domain experts fit + +Many extraction problems are domain-heavy. A developer can usually build the software pipeline, but may not have enough domain knowledge to define intended behavior alone. + +That is why the gold set is so important. + +In practice, the domain expert often contributes in two places: + +1. **Gold-set design** + They help define representative examples, labels, extracted fields, and task boundaries. + +2. **Error review** + They explain what a mistake means in domain terms. + +One useful review pattern is: + +> If a junior analyst with no prior domain experience made this mistake, what would you say to explain what they misunderstood? + +That kind of feedback is especially valuable in LLM workflows, where iteration often happens through prompt improvements, task definitions, extra instructions, or clearer field specifications. + +## What this library replaces + +This library is intended to standardize the repeated evaluation work in the middle of the loop. + +It helps with: + +- comparing predictions to gold data +- aligning rows or entities +- applying feature-specific equality rules +- calculating metrics +- writing logs +- generating basic visualizations + +It is a way to outsource the repeated engineering work in the evaluation layer so that developers do not have to rebuild it for every new workflow. + +## What this library does not replace + +This library does not replace: + +- domain expertise +- gold-set design +- workflow implementation +- error analysis and iteration decisions + +The most important design work still lives in the evaluation set. That is where the task definition, intended behavior, and acceptable tradeoffs are encoded. + +## Why this matters in production + +In many production environments, there is a real tension between speed and quality: + +- shipping fast favors minimal implementation effort +- shipping safely requires an evaluation loop with visible performance + +The goal of this library is to lower that tradeoff. If the evaluation layer is cheap to set up, teams are more likely to keep the discipline of measuring and improving behavior instead of relying on informal spot checks. + +## How the supported task types fit the motivation + +The task types in this library match common evaluation shapes: + +| Real task shape | Typical example | Library task type | +|---|---|---| +| One label per record | sentiment, routing label, final subfolder | `SINGLE_FEATURE` | +| One structured object per record | key fields from an article or form | `SINGLE_ENTITY` | +| Multiple entities per source | contracts, people, products, events in a report | `MULTI_ENTITY` | + +Examples: + +- classifying a text into one final category is usually `SINGLE_FEATURE` +- extracting several key fields from one article is usually `SINGLE_ENTITY` +- extracting multiple contracts from a report is usually `MULTI_ENTITY` +- routing a document into a folder hierarchy can be `SINGLE_FEATURE` or `SINGLE_ENTITY`, depending on whether you score only the final label or the full path + +## A useful mental model + +Think of the library as the reusable evaluation layer around an extraction workflow. + +You still need to: + +- define the gold set +- build the workflow +- improve it over time + +But you do not need to write the comparison, metric, logging, and visualization code from scratch each time. + +## Related pages + +- [Choosing a Task Type](../getting-started/task-selection.md) +- [Task Types](task-types.md) +- [Metrics](metrics.md) diff --git a/docs/concepts/feature-rules.md b/docs/concepts/feature-rules.md new file mode 100644 index 0000000..410ee1f --- /dev/null +++ b/docs/concepts/feature-rules.md @@ -0,0 +1,131 @@ +# Feature Rules + +A `FeatureRule` defines how one feature should be compared. The same rule affects both equality checks for scoring and similarity scoring for entity matching. + +## Field reference + +| Field | Type | Default | Used for | Notes | +|---|---|---|---|---| +| `feature_name` | `str` | required | all tasks | Must match the model/DataFrame column name | +| `feature_type` | `str` | required | all tasks | Must be one of `text`, `number`, `date`, `category` | +| `is_mandatory_for_matching` | `bool` | `True` | exact entity matching | If any mandatory feature differs, exact matching returns similarity `0.0` | +| `weight_for_matching` | `float` | `1.0` | weighted entity matching | Controls contribution to weighted similarity | +| `casefold_text` | `bool` | `True` | text/category | Case-insensitive normalization | +| `strip_text` | `bool` | `True` | text/category | Trims leading and trailing whitespace | +| `remove_punctuation` | `bool` | `True` | text/category | Removes punctuation before comparison | +| `alias_map` | `dict[str, str] \| None` | `None` | text/category, matching | Applied before normalization | +| `numeric_rounding_digits` | `int \| None` | `None` | number | Rounds parsed numeric values before equality | +| `numeric_absolute_tolerance` | `float \| None` | `None` | number | Accepts values within absolute difference | +| `numeric_relative_tolerance` | `float \| None` | `None` | number | Accepts values within relative difference when gold is nonzero | +| `date_tolerance_days` | `int \| None` | `None` | date | Accepts dates within a day window | + +## Allowed `feature_type` values + +`feature_type` is the only field with built-in validation in the model. The current allowed values are: + +- `text` +- `number` +- `date` +- `category` + +Any other value raises `ValueError` during model construction. + +## Text and category semantics + +For `text` and `category` features, the library: + +1. applies `alias_map` if provided +2. converts the value to a string +3. casefolds, strips, removes punctuation, and collapses repeated whitespace +4. compares the normalized strings + +Example: + +```python +FeatureRule( + feature_name="currency_code", + feature_type="category", + alias_map={"US Dollar": "USD"}, +) +``` + +With the default text settings, `"US Dollar"` and `"USD"` compare as equal after aliasing. + +### Current implementation note for missing text values + +The current runtime path stringifies text and category values before normalization. That means `None` is treated like the string `"None"` during comparison, not like a dedicated missing-value sentinel. + +This differs from the number and date behavior, which handle missing values explicitly. + +## Number semantics + +For `number` features, the library: + +1. tries to parse each value as `float` +2. optionally rounds the parsed values +3. treats both-missing as equal +4. treats one-missing and one-present as unequal +5. checks absolute tolerance, then relative tolerance, then exact numeric equality + +Example: + +```python +FeatureRule( + feature_name="contract_amount", + feature_type="number", + numeric_rounding_digits=0, + numeric_absolute_tolerance=100.0, +) +``` + +With this rule, `100000.0` and `100049.0` compare as equal. + +### Missing and unparsable numeric values + +- `None` stays missing +- `NaN` and infinite floats are treated as missing +- unparsable values such as `""` are treated as missing + +## Date semantics + +For `date` features, the library: + +1. parses values with `datetime.fromisoformat(...).date()` +2. treats both-missing as equal +3. treats one-missing and one-present as unequal +4. applies `date_tolerance_days` if configured +5. otherwise requires exact date equality + +Example: + +```python +FeatureRule( + feature_name="publish_date", + feature_type="date", + date_tolerance_days=1, +) +``` + +With this rule, `2024-06-01` and `2024-06-02` compare as equal. + +## Matching behavior + +`FeatureRule` also affects multi-entity matching: + +- in `matching_mode="exact"`, only `is_mandatory_for_matching` matters +- in `matching_mode="weighted"`, every feature contributes a similarity score multiplied by `weight_for_matching` +- text features get partial similarity through token-set overlap +- category, number, and date features contribute either `1.0` or `0.0` + +## Recommended usage + +- Use `alias_map` for known synonyms or canonical value mapping. +- Use tolerances for values where small drift is acceptable. +- Increase `weight_for_matching` on features that are especially identifying in `MULTI_ENTITY` tasks. +- Keep feature names aligned exactly with your Pydantic model fields. + +## Related pages + +- [Run Config](run-config.md) +- [Alignment](alignment.md) +- [Metrics](metrics.md) diff --git a/docs/concepts/logging.md b/docs/concepts/logging.md new file mode 100644 index 0000000..92fc27f --- /dev/null +++ b/docs/concepts/logging.md @@ -0,0 +1,82 @@ +# Logging + +`RunLogger` writes a human-readable text summary for one evaluation run. + +## What `RunLogger` does + +When you instantiate `RunLogger(log_directory_path)`, it immediately ensures the target directory exists. + +When you call `write_log(...)`, it writes a file named: + +```text +test_run_.txt +``` + +The file is written inside `log_directory_path`. + +## Inputs + +`write_log()` takes: + +- a `RunContext` +- a `ResultBundle` +- the `RunConfig` +- an optional `note_message` + +The normal pattern is: + +```python +from extraction_testing import RunLogger, build_run_context + +run_context = build_run_context(run_config) +log_path = RunLogger(run_config.log_directory_path).write_log( + run_context, + result_bundle, + run_config, + note_message="Example run", +) +``` + +## What the log file contains + +Current log content includes: + +- run identifier +- start timestamp +- configuration hash +- task type +- total metrics table +- per-feature metrics table +- row accuracy +- entity detection summary, if present +- optional note line + +## Where the `RunContext` values come from + +`build_run_context(run_config)` creates: + +- `run_identifier`: a timestamp-like string safe for file names +- `started_at_timestamp`: current local timestamp in ISO format +- `configuration_hash`: a deterministic short hash of the run configuration + +The hash is useful when you want to compare runs with slightly different settings. + +## How to interpret a log + +Read it in this order: + +1. confirm the `Task Type` and configuration hash match the run you intended +2. inspect total metrics for a high-level summary +3. inspect per-feature metrics to see which fields are failing +4. inspect row accuracy to understand strict full-row correctness +5. for multi-entity runs, inspect the entity summary to see whether misses are due to matching or field extraction + +## Side effects + +- creates the log directory if it does not already exist +- overwrites any file with the same generated name, though the timestamp-based run identifier makes collisions unlikely + +## Related pages + +- [Run Config](run-config.md) +- [Metrics](metrics.md) diff --git a/docs/concepts/metrics.md b/docs/concepts/metrics.md new file mode 100644 index 0000000..84881fa --- /dev/null +++ b/docs/concepts/metrics.md @@ -0,0 +1,114 @@ +# Metrics + +The library reports two main groups of metrics: + +- feature-level metrics computed on aligned rows +- entity presence metrics for `MULTI_ENTITY` tasks + +## Per-feature metrics + +For each evaluated feature, the library computes: + +| Metric | Meaning | +|---|---| +| `precision` | one-vs-rest precision | +| `recall` | one-vs-rest recall | +| `f1` | harmonic mean of precision and recall | +| `specificity` | one-vs-rest true negative rate | +| `micro_accuracy` | exact-match accuracy across aligned labels | + +By default, these are macro-averaged across all observed labels after normalization. + +## Total metrics + +For multi-feature evaluators, total metrics are the arithmetic mean of the per-feature macro metrics: + +- mean precision +- mean recall +- mean F1 +- mean specificity + +`micro_accuracy` is also included in the per-feature metrics and may appear in the total DataFrame depending on the evaluator path that produced the totals. + +For `SINGLE_FEATURE`, the total metrics row is just the one feature's metrics. + +## Row accuracy + +`row_accuracy_value` answers a stricter question: + +> Among aligned rows, how often were all evaluated features correct at the same time? + +Current behavior: + +- for multi-feature tasks, the evaluator builds a boolean equality matrix and requires every feature in a row to be `True` +- for `SINGLE_FEATURE`, row accuracy reduces to exact label agreement on aligned rows +- if there are no aligned rows, row accuracy is `0.0` + +## Entity presence metrics + +`MULTI_ENTITY` evaluation also produces `entity_detection_summary` with: + +- `predicted_count` +- `gold_count` +- `matched_count` +- `extra_predictions_count` +- `missed_gold_count` +- `precision_entities` +- `recall_entities` +- `f1_entities` + +These metrics are based on matched entities, not on field values inside the entities. + +## Macro vs binary reporting + +The main metric function behaves in two modes: + +- if `classification_config.positive_label` is `None`, return macro precision/recall/F1/specificity across observed labels +- if `positive_label` is set and appears in the label set, return one-vs-rest metrics for that label + +In both cases, `micro_accuracy` remains the fraction of aligned labels that match exactly. + +## Missing-value semantics + +Missing-value handling depends on the feature type. + +### Text and category + +Current implementation behavior: + +- values are converted to strings before normalization +- `None` therefore becomes `"none"` after casefolding +- no special missing-value equality rule is applied + +### Number + +Current implementation behavior: + +- `None`, `NaN`, infinite values, and unparsable numbers are treated as missing +- both missing means equal +- one missing and one present means unequal + +### Date + +Current implementation behavior: + +- unparsable dates are treated as missing +- both missing means equal +- one missing and one present means unequal + +## Important scope note for `MULTI_ENTITY` + +Per-feature metrics and row accuracy are computed only on matched entity pairs. + +That means: + +- a system can have strong per-feature scores on matched entities +- while still having poor entity presence recall because many gold entities were never matched + +Use both metric groups together when evaluating multi-entity extraction quality. + +## Related pages + +- [Task Types](task-types.md) +- [Feature Rules](feature-rules.md) +- [Alignment](alignment.md) diff --git a/docs/concepts/run-config.md b/docs/concepts/run-config.md new file mode 100644 index 0000000..539a612 --- /dev/null +++ b/docs/concepts/run-config.md @@ -0,0 +1,126 @@ +# Run Config + +`RunConfig` is the top-level configuration object passed into `evaluate()`. It tells the orchestrator which evaluator to run and how fields should be compared. + +## `RunConfig` fields + +| Field | Type | Default | Required | Notes | +|---|---|---|---|---| +| `task_type` | `TaskType` | required | yes | Chooses the evaluator | +| `feature_rules` | `list[FeatureRule]` | required | yes | The fields to compare | +| `index_key_name` | `str \| None` | `None` | indexed tasks only | Required for `SINGLE_FEATURE` and `SINGLE_ENTITY` | +| `grouping_key_names` | `list[str] \| None` | `None` | no | Present in the model, not currently used in the runtime path | +| `log_directory_path` | `str` | `"./logs"` | no | Used by `RunLogger` | +| `matching_config` | `MatchingConfig \| None` | `None` | multi-entity only | Defaults are applied if omitted | +| `classification_config` | `ClassificationConfig \| None` | `None` | no | Optional label-reporting behavior | + +## `MatchingConfig` + +`MatchingConfig` is used by `MULTI_ENTITY` tasks. + +| Field | Type | Default | Notes | +|---|---|---|---| +| `matching_mode` | `str` | `"weighted"` | Current implementation expects `"weighted"` or `"exact"` | +| `minimum_similarity_threshold` | `float` | `0.5` | Candidate pairs below this score are discarded | +| `maximum_candidate_pairs` | `int \| None` | `None` | If set, only the top-scoring candidate pairs are kept before greedy matching | +| `random_tie_breaker_seed` | `int` | `13` | Used to make equal-score tie-breaking deterministic | + +### Validation note + +Unlike `FeatureRule.feature_type`, `MatchingConfig` fields are not currently validated beyond normal Pydantic type conversion. Invalid `matching_mode` values do not raise at model creation time; they fall through to the weighted-similarity path. + +## `ClassificationConfig` + +`ClassificationConfig` affects how feature-level label metrics are reported. + +| Field | Type | Default | Notes | +|---|---|---|---| +| `positive_label` | `str \| None` | `None` | If set and observed, the metric function returns one-vs-rest metrics for that label instead of macro metrics | +| `average_strategy` | `str` | `"macro"` | Present in the model but not currently consumed by the metric computation path | + +## Task-specific requirements + +### `SINGLE_FEATURE` + +Use: + +- `task_type=TaskType.SINGLE_FEATURE` +- exactly one `FeatureRule` +- `index_key_name` set + +Current runtime failures: + +- missing `index_key_name` raises `ValueError` +- more than one feature rule raises `ValueError` + +### `SINGLE_ENTITY` + +Use: + +- `task_type=TaskType.SINGLE_ENTITY` +- one or more `FeatureRule` objects +- `index_key_name` set + +Current runtime failure: + +- missing `index_key_name` raises `ValueError` + +### `MULTI_ENTITY` + +Use: + +- `task_type=TaskType.MULTI_ENTITY` +- one or more `FeatureRule` objects +- optional `matching_config` + +`index_key_name` is not required because rows are matched by similarity rather than by an explicit join key. + +## Configuration interactions + +- `task_type` decides which evaluator class the orchestrator instantiates. +- `feature_rules` affect both comparison semantics and, for multi-entity tasks, similarity scoring. +- `matching_config` only matters for `MULTI_ENTITY`. +- `classification_config.positive_label` changes per-feature precision/recall/F1/specificity output from macro to one-vs-rest for that label when the label is present. +- `log_directory_path` does nothing by itself; it is only used if you instantiate `RunLogger`. + +## Common invalid or misleading configurations + +- Setting `matching_config` for an indexed task. + It is harmless but unused. + +- Omitting `index_key_name` for `SINGLE_FEATURE` or `SINGLE_ENTITY`. + The evaluator will fail at runtime. + +- Passing several feature rules to `SINGLE_FEATURE`. + The evaluator expects exactly one feature. + +- Assuming `grouping_key_names` changes result aggregation. + It is currently not used in the execution path. + +- Assuming `average_strategy` switches between macro and micro reporting. + The current metric path does not consume that field. + +## Example + +```python +from extraction_testing import FeatureRule, MatchingConfig, RunConfig, TaskType + +run_config = RunConfig( + task_type=TaskType.MULTI_ENTITY, + feature_rules=[ + FeatureRule(feature_name="contract_title", feature_type="text", weight_for_matching=2.0), + FeatureRule(feature_name="contract_amount", feature_type="number", numeric_absolute_tolerance=100.0), + ], + matching_config=MatchingConfig( + matching_mode="weighted", + minimum_similarity_threshold=0.6, + ), + log_directory_path="./logs", +) +``` + +## Related pages + +- [Task Types](task-types.md) +- [Feature Rules](feature-rules.md) +- [Alignment](alignment.md) diff --git a/docs/concepts/task-types.md b/docs/concepts/task-types.md new file mode 100644 index 0000000..0a8f4df --- /dev/null +++ b/docs/concepts/task-types.md @@ -0,0 +1,71 @@ +# Task Types + +`extraction-testing` supports three task types. They differ mainly in how records are aligned before feature-level comparison starts. + +## Overview + +| Task type | Intended shape | Alignment strategy | `index_key_name` | Entity presence metrics | +|---|---|---|---|---| +| `SINGLE_FEATURE` | One feature per indexed record | `IndexAligner` | Required | No | +| `SINGLE_ENTITY` | Multiple features per indexed record | `IndexAligner` | Required | No | +| `MULTI_ENTITY` | Lists of entities that must be matched | `EntityAligner` | Not required | Yes | + +## `SINGLE_FEATURE` + +Use `SINGLE_FEATURE` when each record has one value you want to score, such as a topic label or one extracted category. + +Current runtime behavior: + +- records are aligned by exact equality on `RunConfig.index_key_name` +- exactly one `FeatureRule` must be provided +- metrics are computed for that one feature +- `row_accuracy_value` is the same idea as exact-label accuracy for the aligned rows + +This is the simplest evaluator shape and usually the best starting point for classification-style outputs. + +## `SINGLE_ENTITY` + +Use `SINGLE_ENTITY` when each indexed record represents one entity with several fields, such as one article with headline, author, and publish date. + +Current runtime behavior: + +- records are aligned by exact equality on `RunConfig.index_key_name` +- one or more `FeatureRule` objects can be provided +- per-feature metrics are computed after alignment +- `row_accuracy_value` is the fraction of aligned rows where every tested feature matches + +This task type assumes the identity of the entity is already known through the index key. + +## `MULTI_ENTITY` + +Use `MULTI_ENTITY` when predicted and gold data are both lists of entities and the evaluator must decide which predicted entity matches which gold entity. + +Current runtime behavior: + +- alignment is handled by `EntityAligner` +- `RunConfig.matching_config` controls exact or weighted matching behavior +- per-feature metrics are computed only on matched pairs +- unmatched predicted and unmatched gold entities are summarized separately in `entity_detection_summary` + +This matters because feature metrics and entity presence metrics answer different questions: + +- feature metrics describe how accurate matched entity fields are +- entity presence metrics describe whether the system found the right entities at all + +## Which aligner each task uses + +- `SINGLE_FEATURE` and `SINGLE_ENTITY` use [`IndexAligner`](../concepts/alignment.md), which joins rows using the configured key. +- `MULTI_ENTITY` uses [`EntityAligner`](../concepts/alignment.md), which scores candidate pairs and then chooses non-overlapping matches. + +## Practical implications + +- If row identity is already known, prefer an indexed task type. +- If row identity must be inferred from content similarity, use `MULTI_ENTITY`. +- If you only care about one output field, use `SINGLE_FEATURE` so the result tables stay minimal. + +## Related pages + +- [Why This Library Exists](evaluation-driven-development.md) +- [Choosing a Task Type](../getting-started/task-selection.md) +- [Run Config](run-config.md) +- [Alignment](alignment.md) diff --git a/docs/concepts/visualization.md b/docs/concepts/visualization.md new file mode 100644 index 0000000..bd0e88b --- /dev/null +++ b/docs/concepts/visualization.md @@ -0,0 +1,91 @@ +# Visualization + +The package includes optional matplotlib-based helpers for turning a `ResultBundle` into simple charts. + +## Dependency model + +Visualization is optional. Install it through the visualization-capable extras: + +- `uv sync --extra examples` +- or `pip install -e ".[viz]"` + +## Available plotting functions + +| Function | Purpose | +|---|---| +| `plot_total_metrics_bar(result_bundle)` | Plot total precision, recall, F1, specificity, micro accuracy, and row accuracy | +| `plot_per_feature_metrics_bar(result_bundle, metric_name="f1")` | Plot one metric per feature | +| `plot_entity_presence_summary(result_bundle)` | Plot entity presence summary if a compatible summary dict is present | +| `plot_confusion_matrix_for_classification(gold_labels, predicted_labels, class_names)` | Plot a confusion matrix from explicit labels | +| `plot_metric_by_group(per_feature_metrics_data_frame, group_column_name, metric_name)` | Plot mean metric by a grouping column in a pre-joined DataFrame | +| `save_all_charts_to_report(result_bundle, output_directory_path)` | Save standard charts into a timestamped report folder | + +## General behavior + +The plotting helpers are intentionally simple: + +- one chart per figure +- deterministic sorting for per-feature and grouped plots +- y-axis constrained to `[0.0, 1.0]` for score-based charts +- empty or invalid inputs usually produce a figure with a clear note instead of failing + +## Report generation + +`save_all_charts_to_report(...)`: + +1. ensures the output directory exists +2. creates a timestamped folder named `report_` +3. saves standard charts as PNG files +4. returns a mapping from chart name to file path + +Current standard outputs: + +- `total_metrics.png` +- `per_feature_f1.png` +- `entity_presence.png` when available + +## Current implementation notes + +### Entity presence summary keys + +`plot_entity_presence_summary()` currently looks for summary keys named: + +- `precision` +- `recall` +- `f1` + +The multi-entity evaluator currently stores: + +- `precision_entities` +- `recall_entities` +- `f1_entities` + +So if you pass the raw `ResultBundle` from `MULTI_ENTITY` evaluation directly, this chart may not display the intended values unless you adapt the summary keys first. + +### Matched-pairs helper expectations + +`extract_labels_for_feature()` expects a `matched_pairs_data_frame` containing columns named like: + +- `_gold` +- `_pred` + +The current evaluator's `matched_pairs_data_frame` contains only indices and similarity scores, so that helper is mainly useful with custom or enriched result bundles. + +## Example + +```python +from extraction_testing.visualization import ( + plot_total_metrics_bar, + plot_per_feature_metrics_bar, + save_all_charts_to_report, +) + +fig_total = plot_total_metrics_bar(result_bundle) +fig_f1 = plot_per_feature_metrics_bar(result_bundle, metric_name="f1") +paths = save_all_charts_to_report(result_bundle, "./_viz_out") +``` + +## Related pages + +- [Metrics](metrics.md) +- [How to Generate Visualizations](../how-to/generate-visualizations.md) diff --git a/docs/contributor/ai-handoff.md b/docs/contributor/ai-handoff.md new file mode 100644 index 0000000..d0d603c --- /dev/null +++ b/docs/contributor/ai-handoff.md @@ -0,0 +1,75 @@ +# AI Handoff + +This page is the operational guide for AI agents working on documentation in this repository. + +## First steps for any docs task + +Before editing docs: + +1. read `AGENTS.md` +2. read `README.md` +3. read `docs/planning/DOCS_ARCHITECTURE_PLAN.md` +4. read `docs/planning/DOCS_PROGRESS.md` +5. inspect the relevant source files before assuming behavior + +Do not rely only on older docs text when the code is easy to inspect directly. + +## Claiming work + +When starting a docs task: + +- move the relevant work package in `DOCS_PROGRESS.md` to `in_progress` +- claim the specific page rows if the work is page-level +- keep ownership explicit so handoffs stay readable + +Do not silently start work without updating the tracker first. + +## While writing + +- keep the current page tree and navigation model intact unless the architecture plan is intentionally changed +- write task-first, concise content +- document current behavior, including caveats +- prefer small runnable examples over abstract pseudo-code when examples help + +If you discover implementation issues while documenting: + +- document the current behavior accurately +- mention the caveat in `DOCS_PROGRESS.md` if it matters for future docs work +- do not silently “correct” documentation to match intended behavior if the code does something else + +## Before finishing + +Every docs agent should: + +1. update `DOCS_PROGRESS.md` +2. move statuses forward only for actually completed work +3. append a new handoff entry +4. list blockers and follow-up recommendations explicitly +5. run a verification pass appropriate to the environment + +## Verification expectations + +Preferred checks: + +- `uv run mkdocs build --strict` +- targeted tests or examples when docs depend on runtime behavior +- file-level checks for placeholder text, broken naming, or nav drift + +If a full docs build is not possible in the current environment, say so in the handoff notes. + +## Handoff note quality + +A good handoff entry should say: + +- what was completed +- which files changed +- which work package statuses changed +- any blockers or caveats +- the recommended next task + +Keep entries short, factual, and easy for the next agent to act on. + +## Related pages + +- [Documentation Style Guide](docs-style-guide.md) +- [Documentation Maintenance](docs-maintenance.md) diff --git a/docs/contributor/docs-maintenance.md b/docs/contributor/docs-maintenance.md new file mode 100644 index 0000000..bcb5d11 --- /dev/null +++ b/docs/contributor/docs-maintenance.md @@ -0,0 +1,103 @@ +# Documentation Maintenance + +This page describes the ongoing maintenance loop for the docs site and how to keep it aligned with the library code. + +## Local docs commands + +Install docs dependencies: + +```bash +uv sync --extra docs +``` + +Serve the docs locally: + +```bash +uv run mkdocs serve +``` + +Build the docs site: + +```bash +uv run mkdocs build --strict +``` + +## When docs must be updated + +Update docs whenever a change affects: + +- public API names or signatures +- task semantics or evaluation behavior +- feature normalization or matching behavior +- logging output or visualization behavior +- installation or development workflow + +At minimum, review: + +- `README.md` +- the relevant page under `docs/` +- any examples affected by the change +- `docs/planning/DOCS_PROGRESS.md` if the change is documentation work + +## Source-of-truth rule + +When docs and code disagree: + +- code wins +- docs should be updated to match the current implementation +- if the implementation is wrong, fix the code and the docs together + +Primary implementation sources: + +- `src/extraction_testing/config.py` +- `src/extraction_testing/orchestrator.py` +- `src/extraction_testing/tests.py` +- `src/extraction_testing/aligners.py` +- `src/extraction_testing/utils.py` +- `src/extraction_testing/metrics.py` +- `src/extraction_testing/logger.py` +- `src/extraction_testing/models.py` +- `src/extraction_testing/visualization.py` + +## Documentation maintenance checklist + +For any meaningful library change: + +1. update the relevant docs pages +2. update any examples that demonstrate the changed behavior +3. review the reference pages for signature or default drift +4. run `uv run mkdocs build --strict` +5. if available, run the relevant tests or examples + +## Publishing workflow + +The repository uses a GitHub Actions workflow for docs publishing: + +- workflow file: `.github/workflows/docs.yml` +- build command: `uv run mkdocs build --strict` +- deployment target: GitHub Pages + +The deploy job is intended for the main branch, while pull requests should still build the docs to catch regressions before merge. + +## Common failure modes + +- page exists in `docs/` but is missing from `mkdocs.yml` +- links point to outdated page paths +- examples use stale enum names or constructor fields +- docs describe planned behavior instead of current behavior +- generated API pages drift because supporting hand-authored summaries were not updated + +## Final QA expectations + +Before considering docs work complete, try to verify: + +- pages exist for all nav entries +- links resolve +- names and terminology are consistent +- examples match current code +- there is no leftover placeholder text + +## Related pages + +- [Documentation Style Guide](docs-style-guide.md) +- [AI Handoff](ai-handoff.md) diff --git a/docs/contributor/docs-style-guide.md b/docs/contributor/docs-style-guide.md new file mode 100644 index 0000000..8501e99 --- /dev/null +++ b/docs/contributor/docs-style-guide.md @@ -0,0 +1,96 @@ +# Documentation Style Guide + +This guide defines how documentation should be written in this repository so it stays readable, easy to maintain, and useful for both humans and AI agents. + +## Core principles + +- Prefer task-first explanations over module-first explanations. +- Keep the code as the source of truth when examples or older docs drift. +- Be explicit about defaults, side effects, and failure modes. +- Keep pages concise, but not so short that important behavior becomes implicit. +- Use runnable examples when they make behavior easier to understand. + +## Audience split + +Write with two audiences in mind: + +- end users who want to evaluate extraction outputs correctly +- maintainers and agents who need predictable structure and precise behavior notes + +This usually means: + +- concepts pages explain semantics +- how-to pages show workflows +- reference pages answer exact API questions +- contributor pages explain process and upkeep + +## Page style + +- Start with the most important answer first. +- Use short sections with informative headings. +- Prefer flat bullet lists over dense paragraphs when listing rules or outcomes. +- Avoid marketing language and avoid filler. +- Keep terminology consistent with the code: `SINGLE_FEATURE`, `SINGLE_ENTITY`, `MULTI_ENTITY`, `FeatureRule`, `RunConfig`, `ResultBundle`. + +## Examples + +Good documentation examples in this repo should be: + +- complete enough to run or adapt +- minimal enough to scan quickly +- consistent with the package's declared Python support +- aligned with the actual public API + +When writing examples: + +- prefer small Pydantic models with only the fields needed for the point +- show the exact `RunConfig` and `FeatureRule` setup that matters +- include a short “what to expect” section when output values are not obvious + +## Handling implementation caveats + +Do not hide surprising behavior. If the code has a current limitation or caveat, document it clearly. + +Examples in this repo include: + +- fields that exist in config models but are not yet used in the runtime path +- differences between intended semantics and current implementation details +- visualization helpers that expect data shapes different from current evaluator outputs + +When possible: + +- describe the current behavior precisely +- avoid promising behavior the code does not implement +- leave future cleanup as a follow-up note rather than papering over it + +## Linking and structure + +- Link from getting-started pages into concepts and reference pages. +- Link from how-to pages back to the concepts they rely on. +- Keep the page tree aligned with `mkdocs.yml`. +- Add new pages intentionally; do not create ad hoc pages outside the planned structure unless the architecture plan is updated first. + +## Reference-page expectations + +Reference pages should answer: + +- what can be imported? +- what are the constructor or function signatures? +- which arguments are required? +- what are the defaults? +- what is returned? +- what side effects or runtime errors should callers expect? + +Use generated API blocks where useful, but keep concise hand-authored summaries above them so the page is still readable if docstrings are sparse. + +## Maintenance discipline + +- Update docs when behavior changes, not later. +- Update examples when public names or semantics change. +- Update `DOCS_PROGRESS.md` for all documentation work. +- Treat unfinished content as explicit backlog, not hidden drift. + +## Related pages + +- [Documentation Maintenance](docs-maintenance.md) +- [AI Handoff](ai-handoff.md) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 0000000..7174634 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,76 @@ +# Installation + +Use `uv` for local development unless you have a reason to use `pip` directly. + +## Core library setup + +Install the package and its core runtime dependencies: + +```bash +uv sync +``` + +This gives you the library with the dependencies needed to run evaluations. + +## Development setup + +Install the package with test, lint, and type-check tooling: + +```bash +uv sync --extra dev +``` + +Run the test suite: + +```bash +uv run pytest +``` + +## Example and visualization dependencies + +Install optional dependencies used by the example scripts and Streamlit demo: + +```bash +uv sync --extra examples +``` + +Example commands: + +```bash +uv run python examples/test.py +uv run python examples/visualize_results.py +uv run streamlit run examples/streamlit_app.py +``` + +## Documentation dependencies + +Install the documentation toolchain: + +```bash +uv sync --extra docs +``` + +Serve the docs site locally: + +```bash +uv run mkdocs serve +``` + +## `pip` alternative + +If you are not using `uv`, install the package in editable mode: + +```bash +pip install -e . +``` + +For development tools: + +```bash +pip install -e ".[dev]" +``` + +## What to read next + +- [Quickstart](quickstart.md) for the smallest realistic evaluation +- [Choosing a Task Type](task-selection.md) if you are not sure which evaluator shape matches your data diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..00f5d33 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,87 @@ +# Quickstart + +This example shows the smallest realistic `SINGLE_FEATURE` evaluation. It is the easiest entry point because it uses one indexed record list, one feature, and no entity matching. + +## Minimal example + +```python +from pydantic import BaseModel + +from extraction_testing import ( + FeatureRule, + RunConfig, + TaskType, + evaluate, +) + + +class ArticleLabel(BaseModel): + row_identifier: int + topic_label: str + + +predicted_records = [ + ArticleLabel(row_identifier=1, topic_label="technology"), + ArticleLabel(row_identifier=2, topic_label="business"), +] + +gold_records = [ + ArticleLabel(row_identifier=1, topic_label="tech"), + ArticleLabel(row_identifier=2, topic_label="business"), +] + +run_config = RunConfig( + task_type=TaskType.SINGLE_FEATURE, + feature_rules=[ + FeatureRule( + feature_name="topic_label", + feature_type="category", + alias_map={"technology": "tech"}, + ) + ], + index_key_name="row_identifier", +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) + +print(result_bundle.per_feature_metrics_data_frame) +print(result_bundle.total_metrics_data_frame) +print(result_bundle.row_accuracy_value) +``` + +## What this example shows + +- Records are passed in as lists of Pydantic models. +- `task_type=TaskType.SINGLE_FEATURE` means exactly one feature should be evaluated. +- `index_key_name` is required because the evaluator aligns rows by exact key match. +- `alias_map` lets equivalent category values compare as equal before metrics are computed. + +## When to use a different setup + +- If each indexed record has several fields to compare, use `SINGLE_ENTITY`. +- If you are comparing lists of entities with no shared row identifier, use `MULTI_ENTITY`. + +See [Choosing a Task Type](task-selection.md) for a side-by-side comparison. + +## Optional: write a log file + +If you want a text summary on disk, add a logger step: + +```python +from extraction_testing import RunLogger, build_run_context + +logger = RunLogger("./logs") +log_path = logger.write_log( + build_run_context(run_config), + result_bundle, + run_config, + note_message="Quickstart run", +) +print(log_path) +``` + +## What to read next + +- [Choosing a Task Type](task-selection.md) +- [Feature Rules](../concepts/feature-rules.md) +- [Run Config](../concepts/run-config.md) diff --git a/docs/getting-started/task-selection.md b/docs/getting-started/task-selection.md new file mode 100644 index 0000000..1705b81 --- /dev/null +++ b/docs/getting-started/task-selection.md @@ -0,0 +1,100 @@ +# Choosing a Task Type + +Choose the task type based on the shape of your records and how alignment should happen before scoring. + +## Decision table + +| If your data looks like this | Use this task type | Alignment strategy | `index_key_name` required | Entity presence metrics | +|---|---|---|---|---| +| One indexed record, one extracted field to compare | `SINGLE_FEATURE` | Exact join on the index key | Yes | No | +| One indexed record, multiple extracted fields to compare | `SINGLE_ENTITY` | Exact join on the index key | Yes | No | +| Lists of predicted and gold entities with no stable row key | `MULTI_ENTITY` | Entity matching, then feature scoring | No | Yes | + +## Input shape examples + +### `SINGLE_FEATURE` + +Use this when each record contributes one label-like value. + +```python +class ArticleLabel(BaseModel): + row_identifier: int + topic_label: str +``` + +Typical use cases: + +- document classification labels +- one extracted category per record +- one scalar field where you want one-feature reporting + +### `SINGLE_ENTITY` + +Use this when each indexed record represents one entity with multiple fields. + +```python +from typing import Optional +from pydantic import BaseModel + + +class ArticleFeatures(BaseModel): + row_identifier: int + headline_text: str + author_name: str + publish_date: Optional[str] +``` + +Typical use cases: + +- one form or document produces one structured object +- you want per-field metrics for a single known entity per record + +### `MULTI_ENTITY` + +Use this when each dataset is a list of entities and the evaluator must decide which predicted entity matches which gold entity. + +```python +from typing import Optional +from pydantic import BaseModel + + +class ContractRecord(BaseModel): + contract_title: str + contract_amount: Optional[float] + contract_date: Optional[str] +``` + +Typical use cases: + +- extracting many entities from one source document set +- invoice line items, contracts, people, products, or events +- no stable shared key exists across predicted and gold outputs + +## Common mistakes + +- Using `SINGLE_FEATURE` with multiple `FeatureRule` objects. + The single-feature evaluator expects exactly one feature rule. + +- Forgetting `index_key_name` for `SINGLE_FEATURE` or `SINGLE_ENTITY`. + Both indexed task types require a join key and raise an error without one. + +- Using `SINGLE_ENTITY` when the real problem is entity matching. + If the evaluator needs to discover which entity matches which, use `MULTI_ENTITY`. + +- Treating `MULTI_ENTITY` as if row order were the identity. + Multi-entity evaluation matches entities by similarity, not by list position. + +## Practical rule of thumb + +Start with the simplest task type that matches your data shape: + +- one field per keyed record: `SINGLE_FEATURE` +- several fields per keyed record: `SINGLE_ENTITY` +- many entities that must be matched first: `MULTI_ENTITY` + +## What to read next + +- [Why This Library Exists](../concepts/evaluation-driven-development.md) +- [Quickstart](quickstart.md) +- [Task Types](../concepts/task-types.md) +- [Run Config](../concepts/run-config.md) diff --git a/docs/how-to/configure-feature-rules.md b/docs/how-to/configure-feature-rules.md new file mode 100644 index 0000000..39cc076 --- /dev/null +++ b/docs/how-to/configure-feature-rules.md @@ -0,0 +1,100 @@ +# Configure Feature Rules + +## Scenario + +You want one evaluation run to compare several feature types with the right normalization rules for each field. + +## Runnable example + +```python +from typing import Optional + +from pydantic import BaseModel + +from extraction_testing import FeatureRule, RunConfig, TaskType, evaluate + + +class ArticleRecord(BaseModel): + row_identifier: int + headline_text: str + view_count: Optional[int] + publish_date: Optional[str] + topic_label: str + + +feature_rules = [ + FeatureRule( + feature_name="headline_text", + feature_type="text", + casefold_text=True, + strip_text=True, + remove_punctuation=True, + ), + FeatureRule( + feature_name="view_count", + feature_type="number", + numeric_absolute_tolerance=5, + numeric_rounding_digits=0, + ), + FeatureRule( + feature_name="publish_date", + feature_type="date", + date_tolerance_days=1, + ), + FeatureRule( + feature_name="topic_label", + feature_type="category", + alias_map={"technology": "tech"}, + ), +] + +predicted_records = [ + ArticleRecord( + row_identifier=1, + headline_text="breaking market rally", + view_count=1003, + publish_date="2024-06-02", + topic_label="technology", + ) +] + +gold_records = [ + ArticleRecord( + row_identifier=1, + headline_text="Breaking: Market Rally", + view_count=1000, + publish_date="2024-06-01", + topic_label="tech", + ) +] + +run_config = RunConfig( + task_type=TaskType.SINGLE_ENTITY, + feature_rules=feature_rules, + index_key_name="row_identifier", +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) +print(result_bundle.per_feature_metrics_data_frame) +``` + +## What this configuration demonstrates + +- `headline_text` uses text normalization to ignore punctuation and case +- `view_count` allows small numeric drift +- `publish_date` allows a one-day difference +- `topic_label` uses an alias map to collapse synonyms + +All four features should compare as equal in this example. + +## Practical selection rules + +- use `text` for free-form fields like titles or names +- use `number` when tolerances or rounding matter +- use `date` when day-window matching matters +- use `category` for canonical labels or enumerated values + +## Related concepts + +- [Feature Rules](../concepts/feature-rules.md) +- [Run Config](../concepts/run-config.md) diff --git a/docs/how-to/evaluate-multi-entity.md b/docs/how-to/evaluate-multi-entity.md new file mode 100644 index 0000000..8f6f385 --- /dev/null +++ b/docs/how-to/evaluate-multi-entity.md @@ -0,0 +1,88 @@ +# Evaluate a Multi-Entity Task + +## Scenario + +You have lists of predicted and gold entities and there is no stable shared key between them. The evaluator must first decide which predicted entity matches which gold entity. + +## Runnable example + +```python +from typing import Optional + +from pydantic import BaseModel + +from extraction_testing.config import MatchingConfig +from extraction_testing import FeatureRule, RunConfig, TaskType, evaluate + + +class ContractRecord(BaseModel): + contract_title: str + contract_amount: Optional[float] + + +predicted_records = [ + ContractRecord(contract_title="Supply Agreement Alpha", contract_amount=100000.0), + ContractRecord(contract_title="Maintenance Contract Beta", contract_amount=50520.0), + ContractRecord(contract_title="Acquisition Accord Zeta", contract_amount=300000.0), +] + +gold_records = [ + ContractRecord(contract_title="Supply Agreement Alpha", contract_amount=100000.0), + ContractRecord(contract_title="Maintenance Contract Beta", contract_amount=50500.0), + ContractRecord(contract_title="Licensing Agreement Epsilon", contract_amount=150000.0), +] + +run_config = RunConfig( + task_type=TaskType.MULTI_ENTITY, + feature_rules=[ + FeatureRule(feature_name="contract_title", feature_type="text", weight_for_matching=2.0), + FeatureRule( + feature_name="contract_amount", + feature_type="number", + numeric_absolute_tolerance=100.0, + weight_for_matching=1.0, + ), + ], + matching_config=MatchingConfig( + matching_mode="weighted", + minimum_similarity_threshold=0.6, + ), +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) + +print(result_bundle.per_feature_metrics_data_frame) +print(result_bundle.total_metrics_data_frame) +print("row_accuracy:", result_bundle.row_accuracy_value) +print(result_bundle.entity_detection_summary) +print(result_bundle.matched_pairs_data_frame) +``` + +## What to expect + +- the first two predicted entities should match the first two gold entities +- the third predicted entity should remain unmatched +- the third gold entity should remain missed + +So the entity summary should show: + +- `matched_count` of `2` +- one extra prediction +- one missed gold entity +- `precision_entities` and `recall_entities` both around `0.6667` + +The matched rows should still score well at the feature level because the amount difference for the beta contract is within the configured tolerance. + +## When this guide applies + +Use this workflow when: + +- entity identity must be inferred from content +- you need both feature-level quality and entity-detection quality +- row order should not be treated as identity + +## Related concepts + +- [Alignment](../concepts/alignment.md) +- [Feature Rules](../concepts/feature-rules.md) +- [Metrics](../concepts/metrics.md) diff --git a/docs/how-to/evaluate-single-entity.md b/docs/how-to/evaluate-single-entity.md new file mode 100644 index 0000000..3cba5ec --- /dev/null +++ b/docs/how-to/evaluate-single-entity.md @@ -0,0 +1,103 @@ +# Evaluate a Single-Entity Task + +## Scenario + +You have one structured entity per record and each entity has several fields. A common example is extracting multiple fields from one article, form, or document. + +## Runnable example + +```python +from typing import Optional + +from pydantic import BaseModel + +from extraction_testing import FeatureRule, RunConfig, TaskType, evaluate + + +class ArticleRecord(BaseModel): + row_identifier: int + headline_text: str + author_name: str + publish_date: Optional[str] + + +predicted_records = [ + ArticleRecord( + row_identifier=1, + headline_text="breaking market rally", + author_name="J. Smith", + publish_date="2024-06-02", + ), + ArticleRecord( + row_identifier=2, + headline_text="Local Sports Win", + author_name="Alex Coach", + publish_date="2024-06-05", + ), +] + +gold_records = [ + ArticleRecord( + row_identifier=1, + headline_text="Breaking: Market Rally", + author_name="John Smith", + publish_date="2024-06-01", + ), + ArticleRecord( + row_identifier=2, + headline_text="Local Sports Win", + author_name="A. Coach", + publish_date="2024-06-01", + ), +] + +run_config = RunConfig( + task_type=TaskType.SINGLE_ENTITY, + feature_rules=[ + FeatureRule(feature_name="headline_text", feature_type="text"), + FeatureRule( + feature_name="author_name", + feature_type="text", + alias_map={"J. Smith": "John Smith", "Alex Coach": "A. Coach"}, + ), + FeatureRule( + feature_name="publish_date", + feature_type="date", + date_tolerance_days=1, + ), + ], + index_key_name="row_identifier", +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) + +print(result_bundle.per_feature_metrics_data_frame) +print(result_bundle.total_metrics_data_frame) +print("row_accuracy:", result_bundle.row_accuracy_value) +``` + +## What to expect + +- the two rows are aligned by `row_identifier` +- headline and author should match on both rows after normalization and aliasing +- `publish_date` should match on row `1` because it is within one day +- `publish_date` should fail on row `2` because it is four days away + +That means: + +- `row_accuracy_value` should be `0.5`, because only the first row is fully correct +- `publish_date` should have weaker metrics than `headline_text` and `author_name` + +## When this guide applies + +Use this workflow when: + +- each record maps to one known entity +- you need per-field scoring across several fields +- alignment should happen by an explicit key, not similarity matching + +## Related concepts + +- [Task Types](../concepts/task-types.md) +- [Feature Rules](../concepts/feature-rules.md) +- [Metrics](../concepts/metrics.md) diff --git a/docs/how-to/evaluate-single-feature.md b/docs/how-to/evaluate-single-feature.md new file mode 100644 index 0000000..c6b6491 --- /dev/null +++ b/docs/how-to/evaluate-single-feature.md @@ -0,0 +1,77 @@ +# Evaluate a Single-Feature Task + +## Scenario + +You have one label-like field per record and a stable row identifier. A common example is topic classification, where each document has one predicted topic label and one gold topic label. + +## Runnable example + +```python +from pydantic import BaseModel + +from extraction_testing import FeatureRule, RunConfig, TaskType, evaluate + + +class ArticleLabel(BaseModel): + row_identifier: int + topic_label: str + + +predicted_records = [ + ArticleLabel(row_identifier=1, topic_label="technology"), + ArticleLabel(row_identifier=2, topic_label="business"), + ArticleLabel(row_identifier=3, topic_label="sports"), +] + +gold_records = [ + ArticleLabel(row_identifier=1, topic_label="tech"), + ArticleLabel(row_identifier=2, topic_label="business"), + ArticleLabel(row_identifier=3, topic_label="politics"), +] + +run_config = RunConfig( + task_type=TaskType.SINGLE_FEATURE, + feature_rules=[ + FeatureRule( + feature_name="topic_label", + feature_type="category", + alias_map={"technology": "tech"}, + ) + ], + index_key_name="row_identifier", +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) + +print(result_bundle.per_feature_metrics_data_frame) +print(result_bundle.total_metrics_data_frame) +print("row_accuracy:", result_bundle.row_accuracy_value) +``` + +## What to expect + +- `per_feature_metrics_data_frame` contains one row for `topic_label` +- `total_metrics_data_frame` is the same one-feature summary in DataFrame form +- `row_accuracy_value` is the fraction of aligned rows where the final label matches exactly + +In this example: + +- row `1` should count as correct because the alias map converts `"technology"` to `"tech"` +- row `2` should count as correct directly +- row `3` should count as incorrect + +So the row accuracy should be about `0.6667`. + +## When this guide applies + +Use this workflow when: + +- you have exactly one feature to score +- record identity is known through a key like `row_identifier` +- you want simple per-label metrics without entity matching + +## Related concepts + +- [Task Types](../concepts/task-types.md) +- [Feature Rules](../concepts/feature-rules.md) +- [Run Config](../concepts/run-config.md) diff --git a/docs/how-to/generate-visualizations.md b/docs/how-to/generate-visualizations.md new file mode 100644 index 0000000..607133b --- /dev/null +++ b/docs/how-to/generate-visualizations.md @@ -0,0 +1,76 @@ +# Generate Visualizations + +## Scenario + +You want quick charts for a finished evaluation run so you can inspect aggregate metrics and per-feature quality visually. + +## Runnable example + +```python +from pydantic import BaseModel + +from extraction_testing import FeatureRule, RunConfig, TaskType, evaluate +from extraction_testing.visualization import ( + plot_per_feature_metrics_bar, + plot_total_metrics_bar, + save_all_charts_to_report, +) + + +class ArticleRecord(BaseModel): + row_identifier: int + headline_text: str + author_name: str + + +predicted_records = [ + ArticleRecord(row_identifier=1, headline_text="Market Rally", author_name="Jane Doe"), + ArticleRecord(row_identifier=2, headline_text="Local Sports Win", author_name="J. Smith"), +] + +gold_records = [ + ArticleRecord(row_identifier=1, headline_text="Market Rally", author_name="Jane Doe"), + ArticleRecord(row_identifier=2, headline_text="Local Sports Win", author_name="John Smith"), +] + +run_config = RunConfig( + task_type=TaskType.SINGLE_ENTITY, + feature_rules=[ + FeatureRule(feature_name="headline_text", feature_type="text"), + FeatureRule( + feature_name="author_name", + feature_type="text", + alias_map={"J. Smith": "John Smith"}, + ), + ], + index_key_name="row_identifier", +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) + +fig_total = plot_total_metrics_bar(result_bundle) +fig_f1 = plot_per_feature_metrics_bar(result_bundle, metric_name="f1") +paths = save_all_charts_to_report(result_bundle, "./_viz_out") + +print(type(fig_total).__name__) +print(type(fig_f1).__name__) +print(paths) +``` + +## What to expect + +- `plot_total_metrics_bar()` returns a matplotlib figure +- `plot_per_feature_metrics_bar()` returns a matplotlib figure for the chosen metric +- `save_all_charts_to_report()` creates a timestamped folder under `./_viz_out` +- the returned `paths` mapping points to the generated PNG files + +## Important current caveat for multi-entity charts + +`plot_entity_presence_summary()` expects summary keys named `precision`, `recall`, and `f1`, while the current multi-entity evaluator emits `precision_entities`, `recall_entities`, and `f1_entities`. + +So if you want an entity-presence chart from a raw multi-entity `ResultBundle`, adapt that summary first or use a custom wrapper object. + +## Related concepts + +- [Visualization](../concepts/visualization.md) +- [Metrics](../concepts/metrics.md) diff --git a/docs/how-to/interpret-results.md b/docs/how-to/interpret-results.md new file mode 100644 index 0000000..0e24e96 --- /dev/null +++ b/docs/how-to/interpret-results.md @@ -0,0 +1,78 @@ +# Interpret Results + +## Scenario + +You already have a `ResultBundle` and need to decide what the tables and summary fields are telling you. + +## Runnable example + +```python +from pydantic import BaseModel + +from extraction_testing import FeatureRule, RunConfig, TaskType, evaluate + + +class ArticleRecord(BaseModel): + row_identifier: int + headline_text: str + author_name: str + + +predicted_records = [ + ArticleRecord(row_identifier=1, headline_text="Market Rally", author_name="Jane Doe"), + ArticleRecord(row_identifier=2, headline_text="Local Sports Win", author_name="J. Smith"), +] + +gold_records = [ + ArticleRecord(row_identifier=1, headline_text="Market Rally", author_name="Jane Doe"), + ArticleRecord(row_identifier=2, headline_text="Local Sports Win", author_name="John Smith"), +] + +run_config = RunConfig( + task_type=TaskType.SINGLE_ENTITY, + feature_rules=[ + FeatureRule(feature_name="headline_text", feature_type="text"), + FeatureRule( + feature_name="author_name", + feature_type="text", + alias_map={"J. Smith": "John Smith"}, + ), + ], + index_key_name="row_identifier", +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) + +print(result_bundle.per_feature_metrics_data_frame) +print(result_bundle.total_metrics_data_frame) +print("row_accuracy:", result_bundle.row_accuracy_value) +``` + +## How to read the result + +- `per_feature_metrics_data_frame` tells you which feature is strong or weak +- `total_metrics_data_frame` gives a compact summary across features +- `row_accuracy_value` tells you how often the entire row was correct at once + +In this example: + +- both features should score perfectly because the author alias makes the second row correct +- `row_accuracy_value` should be `1.0` because both rows are fully correct + +If you removed the alias map: + +- `author_name` would weaken +- total metrics would drop +- row accuracy would also drop because the second row would no longer be fully correct + +## Questions to ask when reading results + +- Is the failure concentrated in one feature or spread across many? +- Is row accuracy much lower than per-feature accuracy? +- For multi-entity tasks, is the real problem entity matching rather than field extraction? + +## Related concepts + +- [Metrics](../concepts/metrics.md) +- [Alignment](../concepts/alignment.md) +- [Task Types](../concepts/task-types.md) diff --git a/docs/how-to/save-logs.md b/docs/how-to/save-logs.md new file mode 100644 index 0000000..78098f6 --- /dev/null +++ b/docs/how-to/save-logs.md @@ -0,0 +1,68 @@ +# Save Logs + +## Scenario + +You want a human-readable record of one evaluation run that you can inspect later or attach to an experiment artifact. + +## Runnable example + +```python +from pydantic import BaseModel + +from extraction_testing import ( + FeatureRule, + RunConfig, + RunLogger, + TaskType, + build_run_context, + evaluate, +) + + +class ArticleLabel(BaseModel): + row_identifier: int + topic_label: str + + +predicted_records = [ArticleLabel(row_identifier=1, topic_label="business")] +gold_records = [ArticleLabel(row_identifier=1, topic_label="business")] + +run_config = RunConfig( + task_type=TaskType.SINGLE_FEATURE, + feature_rules=[FeatureRule(feature_name="topic_label", feature_type="category")], + index_key_name="row_identifier", + log_directory_path="./logs", +) + +result_bundle = evaluate(predicted_records, gold_records, run_config) +run_context = build_run_context(run_config) + +logger = RunLogger(run_config.log_directory_path) +log_path = logger.write_log( + run_context, + result_bundle, + run_config, + note_message="Saved from save-logs guide", +) + +print(log_path) +``` + +## What to expect + +- the `./logs` directory is created automatically if it does not exist +- the file name follows the pattern `test_run_.txt` +- the file includes total metrics, per-feature metrics, row accuracy, and metadata about the run + +## When to use `build_run_context()` + +Use `build_run_context(run_config)` when you want: + +- a timestamp-based run identifier +- a configuration hash for reproducibility +- the metadata expected by `RunLogger.write_log()` + +## Related concepts + +- [Logging](../concepts/logging.md) +- [Run Config](../concepts/run-config.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..df2cb1e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,36 @@ +# extraction-testing + +`extraction-testing` is a typed Python library for evaluating structured extraction outputs against gold data. It supports single-label classification-style checks, single-entity field comparison, and multi-entity matching with per-feature scoring, aggregate metrics, and optional human-readable logs. + +## Supported task types + +- `SINGLE_FEATURE`: evaluate one extracted feature per indexed record +- `SINGLE_ENTITY`: evaluate multiple features for one indexed entity per record +- `MULTI_ENTITY`: evaluate lists of predicted and gold entities using entity matching before scoring + +## Start here + +- [Quickstart](getting-started/quickstart.md) +- [Choosing a Task Type](getting-started/task-selection.md) +- [Why This Library Exists](concepts/evaluation-driven-development.md) +- [API Reference Overview](reference/overview.md) + +## Why this library exists + +This library is designed to make evaluation-driven development practical for extraction workflows. In many teams, building a first workflow is manageable, but building the comparison logic, metric calculations, and reporting layer around it is the part that gets skipped or delayed. + +`extraction-testing` exists to standardize that evaluation layer so teams can spend more effort on the gold set, on the workflow itself, and on structured iteration with domain experts. + +## Evaluation flow + +1. Define one or more `FeatureRule` objects for the fields you want to compare. +2. Build a `RunConfig` with the correct `task_type` and any required alignment settings. +3. Call `evaluate(predicted_records, gold_records, run_config)` with lists of Pydantic models. +4. Inspect the returned `ResultBundle` tables and, if needed, write a text log with `RunLogger`. + +## What the docs cover + +- **Getting Started** shows installation, the shortest working example, and how to choose a task type. +- **Concepts** explains why the library exists and the semantics behind feature normalization, alignment, metrics, logging, and visualization. +- **How-To Guides** provide cookbook-style workflows for common evaluation tasks. +- **API Reference** maps the public package surface and will expand into module-level reference pages. diff --git a/docs/javascripts/mermaid-init.js b/docs/javascripts/mermaid-init.js new file mode 100644 index 0000000..3326bc3 --- /dev/null +++ b/docs/javascripts/mermaid-init.js @@ -0,0 +1,5 @@ +document$.subscribe(function () { + if (typeof mermaid !== "undefined") { + mermaid.initialize({ startOnLoad: true }); + } +}); diff --git a/docs/planning/DOCS_ARCHITECTURE_PLAN.md b/docs/planning/DOCS_ARCHITECTURE_PLAN.md index 6460bbc..f4eaf20 100644 --- a/docs/planning/DOCS_ARCHITECTURE_PLAN.md +++ b/docs/planning/DOCS_ARCHITECTURE_PLAN.md @@ -30,13 +30,13 @@ The documentation should also be usable by AI agents. That means: As of 2026-04-05, the repository has: -- `README.md` with installation, quickstart, metrics overview, and visualization notes +- `README.md` with installation, quickstart, metrics overview, visualization notes, and motivation - `AGENTS.md` with repository guidance for AI agents +- a structured MkDocs documentation site in `docs/` +- concept, how-to, reference, contributor, and planning pages - docstrings in the Python source -- no docs site scaffolding yet -- no `docs/` content for a browser-based documentation site yet, besides this planning area -This means the next phase is not "improve existing docs structure", but "create the first structured docs site intentionally". +This means future work should focus on maintaining alignment between the code and the existing docs structure, not on inventing a new documentation layout. ## Recommended Tooling @@ -83,6 +83,7 @@ docs/ quickstart.md task-selection.md concepts/ + evaluation-driven-development.md task-types.md feature-rules.md run-config.md @@ -132,6 +133,7 @@ Getting Started Quickstart Choosing a Task Type Concepts + Why This Library Exists Task Types Feature Rules Run Config @@ -208,6 +210,16 @@ Must include: - whether `index_key_name` is required - whether entity presence metrics apply +### `concepts/evaluation-driven-development.md` + +Must include: + +- the practical motivation for the library +- a concise explanation of evaluation-driven development +- the role of domain experts and gold-set design +- what work the library standardizes vs what it does not replace +- example mappings from real tasks to `SINGLE_FEATURE`, `SINGLE_ENTITY`, and `MULTI_ENTITY` + ### `concepts/feature-rules.md` Must include: diff --git a/docs/planning/DOCS_PROGRESS.md b/docs/planning/DOCS_PROGRESS.md index 905884b..4934772 100644 --- a/docs/planning/DOCS_PROGRESS.md +++ b/docs/planning/DOCS_PROGRESS.md @@ -41,22 +41,29 @@ Current state: - planning docs exist - information architecture is defined - page tree is defined -- no browser docs tooling has been added yet -- no end-user docs pages have been written yet +- MkDocs scaffolding and nav skeleton exist +- Getting Started pages have been written +- Concepts pages have been written +- How-to guides have been written +- API reference pages have been written +- contributor docs have been written +- GitHub Pages publishing workflow has been added +- strict MkDocs build has passed during final QA +- motivation and EDD docs have been added ## Work Packages | ID | Work package | Status | Owner | Depends on | Notes | |---|---|---|---|---|---| | WP-00 | Planning artifacts | done | current agent | none | `DOCS_ARCHITECTURE_PLAN.md` and `DOCS_PROGRESS.md` created | -| WP-01 | MkDocs site scaffolding | pending | unassigned | WP-00 | Add `mkdocs.yml`, docs plugin/theme config, and nav skeleton | -| WP-02 | Getting Started pages | pending | unassigned | WP-01 | Installation, quickstart, task selection | -| WP-03 | Concepts pages | pending | unassigned | WP-01 | Task types, feature rules, run config, alignment, metrics, logging, visualization | -| WP-04 | API reference pages | pending | unassigned | WP-01 | Prefer generated reference with hand-written overview | -| WP-05 | How-to guides | pending | unassigned | WP-02, WP-03 | One workflow page per common task | -| WP-06 | Contributor docs | pending | unassigned | WP-00 | Docs style guide, maintenance guide, AI handoff guide | -| WP-07 | Docs publishing setup | pending | unassigned | WP-01 | GitHub Pages or equivalent deployment workflow | -| WP-08 | Final docs QA pass | pending | unassigned | WP-02, WP-03, WP-04, WP-05, WP-06 | Link check, naming consistency, examples sanity check | +| WP-01 | MkDocs site scaffolding | done | Codex | WP-00 | Added `mkdocs.yml`, docs extra, nav skeleton, and placeholder pages for the planned tree | +| WP-02 | Getting Started pages | done | Codex | WP-01 | Wrote home, installation, quickstart, and task-selection pages with task-first examples | +| WP-03 | Concepts pages | done | Codex | WP-01 | Wrote all concept pages from current runtime behavior, including validation gaps and visualization caveats | +| WP-04 | API reference pages | done | Codex | WP-01 | Added reference overview pages with hand-authored summaries plus mkdocstrings directives | +| WP-05 | How-to guides | done | Codex | WP-02, WP-03 | Wrote runnable task and workflow guides with expected-result notes | +| WP-06 | Contributor docs | done | Codex | WP-00 | Wrote docs style guide, maintenance guide, and AI handoff guide | +| WP-07 | Docs publishing setup | done | Codex | WP-01 | Added GitHub Pages deployment workflow for MkDocs | +| WP-08 | Final docs QA pass | done | Codex | WP-02, WP-03, WP-04, WP-05, WP-06 | Placeholder scan, diff check, and strict MkDocs build passed | ## Page-Level Backlog @@ -64,56 +71,46 @@ Use this section to claim specific pages if work is split more finely than work | Page | Status | Owner | Notes | |---|---|---|---| -| `docs/index.md` | pending | unassigned | Home page and summary | -| `docs/getting-started/installation.md` | pending | unassigned | `uv`-first setup | -| `docs/getting-started/quickstart.md` | pending | unassigned | Minimal end-to-end example | -| `docs/getting-started/task-selection.md` | pending | unassigned | Decision table for task types | -| `docs/concepts/task-types.md` | pending | unassigned | Must align exactly with current enum names | -| `docs/concepts/feature-rules.md` | pending | unassigned | Full field-by-field semantics | -| `docs/concepts/run-config.md` | pending | unassigned | Task-dependent requirements | -| `docs/concepts/alignment.md` | pending | unassigned | Indexed vs weighted matching | -| `docs/concepts/metrics.md` | pending | unassigned | Macro, micro, row, entity metrics | -| `docs/concepts/logging.md` | pending | unassigned | `RunLogger` behavior | -| `docs/concepts/visualization.md` | pending | unassigned | Plot functions and report generation | -| `docs/how-to/evaluate-single-feature.md` | pending | unassigned | Runnable example | -| `docs/how-to/evaluate-single-entity.md` | pending | unassigned | Runnable example | -| `docs/how-to/evaluate-multi-entity.md` | pending | unassigned | Runnable example | -| `docs/how-to/configure-feature-rules.md` | pending | unassigned | Text/number/date/category examples | -| `docs/how-to/interpret-results.md` | pending | unassigned | Explain result bundle tables and metrics | -| `docs/how-to/save-logs.md` | pending | unassigned | Logging workflow | -| `docs/how-to/generate-visualizations.md` | pending | unassigned | Chart generation workflow | -| `docs/reference/overview.md` | pending | unassigned | Public API map | -| `docs/reference/config.md` | pending | unassigned | Config models and enums | -| `docs/reference/orchestrator.md` | pending | unassigned | `evaluate`, `build_run_context` | -| `docs/reference/logger.md` | pending | unassigned | `RunLogger` | -| `docs/reference/visualization.md` | pending | unassigned | Plot/save functions | -| `docs/reference/models.md` | pending | unassigned | `ResultBundle`, `RunContext` | -| `docs/contributor/docs-style-guide.md` | pending | unassigned | Writing conventions | -| `docs/contributor/docs-maintenance.md` | pending | unassigned | How to keep docs in sync | -| `docs/contributor/ai-handoff.md` | pending | unassigned | Agent-facing quick handoff guide | +| `docs/index.md` | done | Codex | Home page and summary written | +| `docs/getting-started/installation.md` | done | Codex | `uv`-first setup, extras, tests, and docs commands included | +| `docs/getting-started/quickstart.md` | done | Codex | Minimal `SINGLE_FEATURE` example plus optional logging step | +| `docs/getting-started/task-selection.md` | done | Codex | Decision table, shape examples, and common mistakes | +| `docs/concepts/task-types.md` | done | Codex | Task semantics, aligners, and metric scope documented | +| `docs/concepts/feature-rules.md` | done | Codex | Field-by-field semantics, defaults, and missing-value behavior documented | +| `docs/concepts/run-config.md` | done | Codex | Task-dependent requirements and currently unused fields documented | +| `docs/concepts/alignment.md` | done | Codex | Indexed vs weighted matching, thresholds, and determinism documented | +| `docs/concepts/metrics.md` | done | Codex | Macro, binary, row, entity, and missing-value semantics documented | +| `docs/concepts/logging.md` | done | Codex | `RunLogger` inputs, outputs, and side effects documented | +| `docs/concepts/visualization.md` | done | Codex | Plot functions, report output, and current implementation caveats documented | +| `docs/concepts/evaluation-driven-development.md` | done | Codex | Motivation page explaining why the library exists and how it fits EDD | +| `docs/how-to/evaluate-single-feature.md` | done | Codex | Runnable example with expected row accuracy | +| `docs/how-to/evaluate-single-entity.md` | done | Codex | Runnable example with field-level interpretation | +| `docs/how-to/evaluate-multi-entity.md` | done | Codex | Runnable example with entity-summary interpretation | +| `docs/how-to/configure-feature-rules.md` | done | Codex | Complete feature-rule setup example across feature types | +| `docs/how-to/interpret-results.md` | done | Codex | Result-bundle reading guide with comparison example | +| `docs/how-to/save-logs.md` | done | Codex | End-to-end logging workflow | +| `docs/how-to/generate-visualizations.md` | done | Codex | Plot creation and report-saving workflow | +| `docs/reference/overview.md` | done | Codex | Public API map and import surface documented | +| `docs/reference/config.md` | done | Codex | Config models and enums documented with mkdocstrings hook | +| `docs/reference/orchestrator.md` | done | Codex | `evaluate` and `build_run_context` documented | +| `docs/reference/logger.md` | done | Codex | `RunLogger` documented | +| `docs/reference/visualization.md` | done | Codex | Plot/save functions documented with caveats | +| `docs/reference/models.md` | done | Codex | Result dataclasses documented | +| `docs/contributor/docs-style-guide.md` | done | Codex | Writing conventions and docs-quality rules documented | +| `docs/contributor/docs-maintenance.md` | done | Codex | Local docs workflow, sync points, and QA checklist documented | +| `docs/contributor/ai-handoff.md` | done | Codex | Agent workflow for claiming, verifying, and handing off docs work documented | ## Current Blockers -No blocking technical issue yet. - -Open design decisions that may block later work: - -- generated vs hand-authored API reference details -- exact docs theme/tooling choice -- docs deployment workflow +No current blocker. ## Recommended Next Steps -Recommended implementation order: +Recommended maintenance and enhancement work: -1. WP-01: set up MkDocs scaffolding and create empty page files -2. WP-02: write Getting Started pages -3. WP-03: write Concepts pages -4. WP-04: build API reference pages -5. WP-05: add How-to guides -6. WP-06: add Contributor docs -7. WP-07: wire browser publishing -8. WP-08: final QA pass +1. keep README and docs pages aligned when evaluation semantics change +2. keep reference-page summaries aligned with generated API details and docstrings +3. consider adding stronger automated docs QA beyond `mkdocs build --strict` if link validation becomes important ## Handoff Log @@ -133,6 +130,134 @@ Append new entries at the bottom. - Recommended next task: - implement WP-01 and create the actual MkDocs scaffolding +### 2026-04-05 - Codex + +- Completed: + - implemented WP-01 MkDocs scaffolding + - implemented WP-02 Getting Started pages +- Files added/updated: + - `mkdocs.yml` + - `pyproject.toml` + - `docs/index.md` + - `docs/getting-started/installation.md` + - `docs/getting-started/quickstart.md` + - `docs/getting-started/task-selection.md` + - placeholder pages across `docs/concepts/`, `docs/how-to/`, `docs/reference/`, and `docs/contributor/` + - `docs/planning/DOCS_PROGRESS.md` +- Work package status changes: + - `WP-01` -> `done` + - `WP-02` -> `done` +- Blockers: + - no technical blocker for writing the next docs pages + - API reference strategy is still only partially resolved; current reference pages are placeholders until WP-04 +- Follow-up recommendations: + - write WP-03 Concepts pages next, starting with `task-types`, `feature-rules`, and `run-config` + - then replace API reference placeholders with mkdocstrings-backed pages in WP-04 + +### 2026-04-05 - Codex + +- Completed: + - implemented WP-03 Concepts pages +- Files added/updated: + - `docs/concepts/task-types.md` + - `docs/concepts/feature-rules.md` + - `docs/concepts/run-config.md` + - `docs/concepts/alignment.md` + - `docs/concepts/metrics.md` + - `docs/concepts/logging.md` + - `docs/concepts/visualization.md` + - `docs/planning/DOCS_PROGRESS.md` +- Work package status changes: + - `WP-03` -> `done` +- Blockers: + - no blocker for continuing into how-to or reference docs + - some current runtime behaviors are now documented as-is and may warrant future code cleanup: + - text/category `None` handling is string-based + - `grouping_key_names` and `average_strategy` are present but unused + - visualization entity-summary key expectations do not match evaluator output keys +- Follow-up recommendations: + - write WP-05 how-to guides next while the concept pages are fresh + - handle WP-04 after deciding how much of the API reference should be generated with mkdocstrings vs hand-authored + +### 2026-04-05 - Codex + +- Completed: + - implemented WP-05 How-to guides + - implemented WP-04 API reference pages +- Files added/updated: + - `docs/how-to/evaluate-single-feature.md` + - `docs/how-to/evaluate-single-entity.md` + - `docs/how-to/evaluate-multi-entity.md` + - `docs/how-to/configure-feature-rules.md` + - `docs/how-to/interpret-results.md` + - `docs/how-to/save-logs.md` + - `docs/how-to/generate-visualizations.md` + - `docs/reference/overview.md` + - `docs/reference/config.md` + - `docs/reference/orchestrator.md` + - `docs/reference/logger.md` + - `docs/reference/visualization.md` + - `docs/reference/models.md` + - `docs/planning/DOCS_PROGRESS.md` +- Work package status changes: + - `WP-05` -> `done` + - `WP-04` -> `done` +- Blockers: + - no blocker for continuing to contributor docs or publishing setup + - mkdocstrings-backed pages have not been validated with a full docs build in this turn +- Follow-up recommendations: + - write WP-06 contributor docs next + - then run a docs build / QA pass once the remaining sections are in place + +### 2026-04-05 - Codex + +- Completed: + - implemented WP-06 contributor docs + - implemented WP-07 docs publishing setup + - implemented WP-08 final docs QA pass +- Files added/updated: + - `docs/contributor/docs-style-guide.md` + - `docs/contributor/docs-maintenance.md` + - `docs/contributor/ai-handoff.md` + - `mkdocs.yml` + - `.github/workflows/docs.yml` + - `docs/planning/DOCS_PROGRESS.md` +- Work package status changes: + - `WP-06` -> `done` + - `WP-07` -> `done` + - `WP-08` -> `done` +- Blockers: + - no remaining blocker for the planned docs rollout + - note: the final build showed an upstream Material for MkDocs warning about MkDocs 2.0; it did not block the current build +- Follow-up recommendations: + - if you want tighter CI validation later, consider adding explicit link-validation settings or a dedicated docs QA workflow step beyond `mkdocs build --strict` + +### 2026-04-05 - Codex + +- Completed: + - added motivation content to the README and docs home page + - added a dedicated EDD motivation concepts page + - added Mermaid support for a documentation workflow diagram + - updated adjacent docs and planning files for consistency +- Files added/updated: + - `README.md` + - `mkdocs.yml` + - `pyproject.toml` + - `docs/index.md` + - `docs/getting-started/task-selection.md` + - `docs/concepts/task-types.md` + - `docs/concepts/evaluation-driven-development.md` + - `docs/javascripts/mermaid-init.js` + - `docs/planning/DOCS_ARCHITECTURE_PLAN.md` + - `docs/planning/DOCS_PROGRESS.md` +- Work package status changes: + - no existing work package changed; this was a post-rollout documentation enhancement +- Blockers: + - no blocker + - note: the strict build still shows the existing upstream Material for MkDocs warning about MkDocs 2.0, but the build passes +- Follow-up recommendations: + - if you add more high-level conceptual pages later, keep the README summary shorter than the dedicated docs page and let the concepts section carry the full explanation + ## Handoff Entry Template Copy this section for future updates: diff --git a/docs/reference/config.md b/docs/reference/config.md new file mode 100644 index 0000000..656b2a6 --- /dev/null +++ b/docs/reference/config.md @@ -0,0 +1,60 @@ +# `config` + +This module defines the task enum and the configuration models used to control evaluation. + +## Key symbols + +### `TaskType` + +Allowed enum values: + +- `SINGLE_FEATURE` +- `SINGLE_ENTITY` +- `MULTI_ENTITY` + +### `FeatureRule` + +Constructor highlights: + +- required: `feature_name`, `feature_type` +- validated allowed `feature_type` values: `text`, `number`, `date`, `category` +- side effect: none +- error conditions: invalid `feature_type` raises `ValueError` + +### `MatchingConfig` + +Constructor highlights: + +- defaults to weighted matching with threshold `0.5` +- used only by `MULTI_ENTITY` +- side effect: none +- current caveat: `matching_mode` is not explicitly validated beyond normal type coercion + +### `ClassificationConfig` + +Constructor highlights: + +- `positive_label` switches the metric function into one-vs-rest mode for that label when present +- `average_strategy` exists in the model but is not currently consumed in the metric path + +### `RunConfig` + +Constructor highlights: + +- required: `task_type`, `feature_rules` +- `index_key_name` is required at runtime for indexed task types +- `matching_config` is relevant only for `MULTI_ENTITY` +- `log_directory_path` controls where `RunLogger` writes files +- `grouping_key_names` exists but is not currently used in the runtime path + +## Generated API details + +::: extraction_testing.config + options: + members: + - TaskType + - FeatureRule + - MatchingConfig + - ClassificationConfig + - RunConfig + show_root_heading: false diff --git a/docs/reference/logger.md b/docs/reference/logger.md new file mode 100644 index 0000000..9ad397f --- /dev/null +++ b/docs/reference/logger.md @@ -0,0 +1,39 @@ +# `logger` + +This module defines `RunLogger`, the package's human-readable text log writer. + +## `RunLogger` + +Constructor: + +- `RunLogger(log_directory_path: str)` + +Behavior: + +- stores the target directory path +- ensures the directory exists immediately + +Main method: + +- `write_log(run_context, result_bundle, run_config, note_message=None) -> str` + +Returns: + +- the path to the written log file as a string + +Side effects: + +- creates the log directory if needed +- writes a text file to disk + +Error conditions: + +- the module does not perform much explicit validation; file-system errors would surface from the underlying write path + +## Generated API details + +::: extraction_testing.logger + options: + members: + - RunLogger + show_root_heading: false diff --git a/docs/reference/models.md b/docs/reference/models.md new file mode 100644 index 0000000..7d4147f --- /dev/null +++ b/docs/reference/models.md @@ -0,0 +1,50 @@ +# `models` + +This module defines the lightweight dataclasses returned by the evaluation pipeline. + +## `ConfusionCounts` + +Fields: + +- `true_positive_count` +- `false_positive_count` +- `true_negative_count` +- `false_negative_count` + +Used internally by the metric helpers. + +## `ResultBundle` + +Fields: + +- `per_feature_metrics_data_frame` +- `total_metrics_data_frame` +- `row_accuracy_value` +- `entity_detection_summary=None` +- `matched_pairs_data_frame=None` + +Notes: + +- returned by `evaluate()` +- `entity_detection_summary` is normally populated only for `MULTI_ENTITY` +- `matched_pairs_data_frame` currently contains pair indices and similarity scores for multi-entity runs + +## `RunContext` + +Fields: + +- `run_identifier` +- `started_at_timestamp` +- `configuration_hash` + +Used mainly for logging and run traceability. + +## Generated API details + +::: extraction_testing.models + options: + members: + - ConfusionCounts + - ResultBundle + - RunContext + show_root_heading: false diff --git a/docs/reference/orchestrator.md b/docs/reference/orchestrator.md new file mode 100644 index 0000000..1c46f7b --- /dev/null +++ b/docs/reference/orchestrator.md @@ -0,0 +1,54 @@ +# `orchestrator` + +This module contains the main user-facing entry points for running evaluations. + +## `evaluate(predicted_records, gold_records, run_config)` + +Purpose: + +- choose the correct evaluator class from `run_config.task_type` +- run the evaluation +- return a `ResultBundle` + +Parameters: + +- `predicted_records`: list of Pydantic models representing predictions +- `gold_records`: list of Pydantic models representing gold data +- `run_config`: `RunConfig` controlling task type and comparison behavior + +Returns: + +- `ResultBundle` + +Error conditions: + +- unsupported `task_type` raises `ValueError` +- indexed task types raise `ValueError` if `index_key_name` is missing +- single-feature evaluation raises `ValueError` if more than one feature rule is supplied + +Side effects: + +- none + +## `build_run_context(run_config)` + +Purpose: + +- create a `RunContext` for logging and traceability + +Returns: + +- `RunContext` containing a run identifier, start timestamp, and configuration hash + +Side effects: + +- none + +## Generated API details + +::: extraction_testing.orchestrator + options: + members: + - build_run_context + - evaluate + show_root_heading: false diff --git a/docs/reference/overview.md b/docs/reference/overview.md new file mode 100644 index 0000000..03bd095 --- /dev/null +++ b/docs/reference/overview.md @@ -0,0 +1,58 @@ +# API Reference Overview + +This section documents the current public API surface exposed by `extraction_testing`. + +## Public import surface + +The package root currently re-exports: + +- `TaskType` +- `FeatureRule` +- `MatchingConfig` +- `ClassificationConfig` +- `RunConfig` +- `ResultBundle` +- `RunContext` +- `evaluate` +- `build_run_context` +- `RunLogger` + +Typical import pattern: + +```python +from extraction_testing import ( + ClassificationConfig, + FeatureRule, + MatchingConfig, + ResultBundle, + RunConfig, + RunContext, + RunLogger, + TaskType, + build_run_context, + evaluate, +) +``` + +## Module guide + +- [config](config.md): enums and configuration models +- [orchestrator](orchestrator.md): the main evaluation entry points +- [logger](logger.md): text log writer +- [visualization](visualization.md): optional plotting helpers +- [models](models.md): result and metadata dataclasses + +## What to expect from these pages + +Each reference page is organized to answer the same set of questions: + +- what can I import? +- what is the function or constructor signature? +- which arguments are required? +- what are the defaults and allowed values? +- what is returned? +- what side effects or error conditions should I expect? + +## Notes on authority + +The reference pages describe the current implementation. If an example or higher-level concept page ever disagrees with a signature or runtime behavior, the code in `src/extraction_testing/` is the source of truth. diff --git a/docs/reference/visualization.md b/docs/reference/visualization.md new file mode 100644 index 0000000..6615903 --- /dev/null +++ b/docs/reference/visualization.md @@ -0,0 +1,44 @@ +# `visualization` + +This module contains optional matplotlib-based plotting helpers. + +## Plotting helpers + +Main functions: + +- `plot_total_metrics_bar(result_bundle)` +- `plot_per_feature_metrics_bar(result_bundle, metric_name="f1")` +- `plot_entity_presence_summary(result_bundle)` +- `plot_confusion_matrix_for_classification(gold_labels, predicted_labels, class_names)` +- `plot_metric_by_group(per_feature_metrics_data_frame, group_column_name, metric_name)` +- `save_all_charts_to_report(result_bundle, output_directory_path)` + +Typical return type: + +- most plotting functions return `matplotlib.figure.Figure` +- `save_all_charts_to_report()` returns `dict[str, str]` + +Important current caveats: + +- `plot_entity_presence_summary()` expects summary keys named `precision`, `recall`, and `f1` +- the multi-entity evaluator currently emits `precision_entities`, `recall_entities`, and `f1_entities` +- `extract_labels_for_feature()` expects enriched matched-pair columns that are not produced by the current evaluator output + +## Error conditions + +- invalid `metric_name` for per-feature plots raises `ValueError` +- confusion-matrix plotting raises `ValueError` for length mismatches, empty class lists, or missing observed labels + +## Generated API details + +::: extraction_testing.visualization + options: + members: + - plot_total_metrics_bar + - plot_per_feature_metrics_bar + - plot_entity_presence_summary + - plot_confusion_matrix_for_classification + - plot_metric_by_group + - extract_labels_for_feature + - save_all_charts_to_report + show_root_heading: false diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c2e8163 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,64 @@ +site_name: extraction-testing +site_description: Documentation for the extraction-testing evaluation library. +repo_name: extraction-testing +docs_dir: docs +strict: true +theme: + name: material +plugins: + - search + - mkdocstrings: + handlers: + python: + paths: + - src +markdown_extensions: + - admonition + - tables + - toc: + permalink: true + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format +extra_javascript: + - https://unpkg.com/mermaid@10/dist/mermaid.min.js + - javascripts/mermaid-init.js +nav: + - Home: index.md + - Getting Started: + - Installation: getting-started/installation.md + - Quickstart: getting-started/quickstart.md + - Choosing a Task Type: getting-started/task-selection.md + - Concepts: + - Why This Library Exists: concepts/evaluation-driven-development.md + - Task Types: concepts/task-types.md + - Feature Rules: concepts/feature-rules.md + - Run Config: concepts/run-config.md + - Alignment: concepts/alignment.md + - Metrics: concepts/metrics.md + - Logging: concepts/logging.md + - Visualization: concepts/visualization.md + - How-To Guides: + - Evaluate a Single-Feature Task: how-to/evaluate-single-feature.md + - Evaluate a Single-Entity Task: how-to/evaluate-single-entity.md + - Evaluate a Multi-Entity Task: how-to/evaluate-multi-entity.md + - Configure Feature Rules: how-to/configure-feature-rules.md + - Interpret Results: how-to/interpret-results.md + - Save Logs: how-to/save-logs.md + - Generate Visualizations: how-to/generate-visualizations.md + - API Reference: + - Overview: reference/overview.md + - config: reference/config.md + - orchestrator: reference/orchestrator.md + - logger: reference/logger.md + - visualization: reference/visualization.md + - models: reference/models.md + - Contributor Docs: + - Documentation Style Guide: contributor/docs-style-guide.md + - Documentation Maintenance: contributor/docs-maintenance.md + - AI Handoff: contributor/ai-handoff.md + - Planning: + - Docs Architecture Plan: planning/DOCS_ARCHITECTURE_PLAN.md + - Docs Progress: planning/DOCS_PROGRESS.md diff --git a/pyproject.toml b/pyproject.toml index 652d5b4..0df7709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,19 @@ examples = [ "matplotlib>=3.8", "streamlit>=1.35", ] +docs = [ + "mkdocs>=1.6", + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.26", + "pymdown-extensions>=10.0", +] dev = [ "black>=24.0", "matplotlib>=3.8", + "mkdocs>=1.6", + "mkdocs-material>=9.5", + "mkdocstrings[python]>=0.26", + "pymdown-extensions>=10.0", "mypy>=1.5", "pytest>=8", "pytest-cov>=5", diff --git a/uv.lock b/uv.lock index db2ce9b..2db6642 100644 --- a/uv.lock +++ b/uv.lock @@ -74,6 +74,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, +] + [[package]] name = "black" version = "25.11.0" @@ -911,8 +934,13 @@ dev = [ { name = "black", version = "26.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.10'" }, { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "mypy", version = "1.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pymdown-extensions" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pytest-cov" }, @@ -920,6 +948,13 @@ dev = [ { name = "types-python-dateutil", version = "2.9.0.20260124", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "types-python-dateutil", version = "2.9.0.20260402", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, ] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.10'" }, + { name = "pymdown-extensions" }, +] examples = [ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, @@ -937,17 +972,25 @@ requires-dist = [ { name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.8" }, { name = "matplotlib", marker = "extra == 'examples'", specifier = ">=3.8" }, { name = "matplotlib", marker = "extra == 'viz'", specifier = ">=3.8" }, + { name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6" }, + { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6" }, + { name = "mkdocs-material", marker = "extra == 'dev'", specifier = ">=9.5" }, + { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev'", specifier = ">=0.26" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'", specifier = ">=0.26" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.5" }, { name = "numpy", specifier = ">=1.23" }, { name = "pandas", specifier = ">=1.5" }, { name = "pydantic", specifier = ">=1.10,<3" }, + { name = "pymdown-extensions", marker = "extra == 'dev'", specifier = ">=10.0" }, + { name = "pymdown-extensions", marker = "extra == 'docs'", specifier = ">=10.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5" }, { name = "streamlit", marker = "extra == 'examples'", specifier = ">=1.35" }, { name = "types-python-dateutil", marker = "extra == 'dev'", specifier = ">=2.9.0.20240316" }, ] -provides-extras = ["viz", "examples", "dev"] +provides-extras = ["viz", "examples", "docs", "dev"] [[package]] name = "fonttools" @@ -1083,6 +1126,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -1108,6 +1163,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, ] +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1117,6 +1193,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + [[package]] name = "importlib-resources" version = "6.5.2" @@ -1559,6 +1647,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/36/18f6e768afad6b55a690d38427c53251b69b7ba8795512730fd2508b31a9/librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7", size = 61507, upload-time = "2026-02-17T16:13:04.556Z" }, ] +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1802,6 +1923,206 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "mergedeep" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "platformdirs", version = "4.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "requests", version = "2.33.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "jinja2", marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markupsafe", marker = "python_full_version < '3.10'" }, + { name = "mkdocs", marker = "python_full_version < '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, + { name = "pymdown-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", marker = "python_full_version >= '3.10'" }, + { name = "mkdocs", marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, + { name = "pymdown-extensions", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "2.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "griffe", marker = "python_full_version < '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version < '3.10'" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "griffelib", marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + [[package]] name = "mypy" version = "1.19.1" @@ -2179,6 +2500,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pandas" version = "2.3.3" @@ -2924,6 +3254,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown", version = "3.10.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -3062,6 +3406,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] +[[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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -3740,6 +4169,25 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },