agent-once 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. agent_once-0.1.0/.github/workflows/ci.yml +119 -0
  2. agent_once-0.1.0/.github/workflows/docs.yml +57 -0
  3. agent_once-0.1.0/.github/workflows/publish.yml +42 -0
  4. agent_once-0.1.0/.gitignore +17 -0
  5. agent_once-0.1.0/CHANGELOG.md +45 -0
  6. agent_once-0.1.0/CONTRIBUTING.md +36 -0
  7. agent_once-0.1.0/LICENSE +21 -0
  8. agent_once-0.1.0/PKG-INFO +421 -0
  9. agent_once-0.1.0/README.md +371 -0
  10. agent_once-0.1.0/SECURITY.md +9 -0
  11. agent_once-0.1.0/docs/backends.md +83 -0
  12. agent_once-0.1.0/docs/cli.md +36 -0
  13. agent_once-0.1.0/docs/concepts.md +128 -0
  14. agent_once-0.1.0/docs/hooks.md +70 -0
  15. agent_once-0.1.0/docs/index.md +49 -0
  16. agent_once-0.1.0/docs/integrations/crewai.md +50 -0
  17. agent_once-0.1.0/docs/integrations/index.md +20 -0
  18. agent_once-0.1.0/docs/integrations/langchain.md +55 -0
  19. agent_once-0.1.0/docs/integrations/mcp.md +57 -0
  20. agent_once-0.1.0/docs/integrations/openai-agents.md +71 -0
  21. agent_once-0.1.0/docs/integrations/pydantic-ai.md +61 -0
  22. agent_once-0.1.0/docs/quickstart.md +88 -0
  23. agent_once-0.1.0/docs/reference/integrations.md +21 -0
  24. agent_once-0.1.0/docs/reference/once.md +35 -0
  25. agent_once-0.1.0/docs/reference/stores.md +11 -0
  26. agent_once-0.1.0/docs/reference/verifiers.md +7 -0
  27. agent_once-0.1.0/docs/ttl.md +68 -0
  28. agent_once-0.1.0/docs/verifiers.md +110 -0
  29. agent_once-0.1.0/docs/why-not.md +34 -0
  30. agent_once-0.1.0/examples/crewai_example.py +40 -0
  31. agent_once-0.1.0/examples/langgraph_example.py +58 -0
  32. agent_once-0.1.0/examples/mcp_server.py +55 -0
  33. agent_once-0.1.0/examples/openai_agents_example.py +50 -0
  34. agent_once-0.1.0/examples/pydantic_ai_example.py +51 -0
  35. agent_once-0.1.0/examples/stripe_example.py +74 -0
  36. agent_once-0.1.0/mkdocs.yml +74 -0
  37. agent_once-0.1.0/pyproject.toml +79 -0
  38. agent_once-0.1.0/requirements-docs.txt +4 -0
  39. agent_once-0.1.0/src/once/__init__.py +38 -0
  40. agent_once-0.1.0/src/once/__main__.py +5 -0
  41. agent_once-0.1.0/src/once/_otel.py +57 -0
  42. agent_once-0.1.0/src/once/cli.py +172 -0
  43. agent_once-0.1.0/src/once/core.py +1088 -0
  44. agent_once-0.1.0/src/once/exceptions.py +53 -0
  45. agent_once-0.1.0/src/once/hooks.py +104 -0
  46. agent_once-0.1.0/src/once/integrations/__init__.py +1 -0
  47. agent_once-0.1.0/src/once/integrations/_wrap.py +81 -0
  48. agent_once-0.1.0/src/once/integrations/crewai.py +90 -0
  49. agent_once-0.1.0/src/once/integrations/langchain.py +102 -0
  50. agent_once-0.1.0/src/once/integrations/mcp.py +114 -0
  51. agent_once-0.1.0/src/once/integrations/openai_agents.py +118 -0
  52. agent_once-0.1.0/src/once/integrations/pydantic_ai.py +123 -0
  53. agent_once-0.1.0/src/once/key.py +32 -0
  54. agent_once-0.1.0/src/once/postgres.py +316 -0
  55. agent_once-0.1.0/src/once/py.typed +1 -0
  56. agent_once-0.1.0/src/once/store.py +405 -0
  57. agent_once-0.1.0/src/once/verifiers/__init__.py +11 -0
  58. agent_once-0.1.0/src/once/verifiers/_http.py +92 -0
  59. agent_once-0.1.0/src/once/verifiers/_stripe.py +237 -0
  60. agent_once-0.1.0/tests/conftest.py +48 -0
  61. agent_once-0.1.0/tests/test_async.py +382 -0
  62. agent_once-0.1.0/tests/test_basic.py +279 -0
  63. agent_once-0.1.0/tests/test_cli.py +145 -0
  64. agent_once-0.1.0/tests/test_concurrency.py +160 -0
  65. agent_once-0.1.0/tests/test_crash.py +116 -0
  66. agent_once-0.1.0/tests/test_edges.py +182 -0
  67. agent_once-0.1.0/tests/test_hooks.py +398 -0
  68. agent_once-0.1.0/tests/test_integrations.py +155 -0
  69. agent_once-0.1.0/tests/test_integrations_adapters.py +275 -0
  70. agent_once-0.1.0/tests/test_integrations_crewai.py +67 -0
  71. agent_once-0.1.0/tests/test_integrations_langchain.py +52 -0
  72. agent_once-0.1.0/tests/test_integrations_mcp.py +58 -0
  73. agent_once-0.1.0/tests/test_integrations_openai_agents.py +52 -0
  74. agent_once-0.1.0/tests/test_integrations_pydantic_ai.py +66 -0
  75. agent_once-0.1.0/tests/test_integrations_stripe.py +165 -0
  76. agent_once-0.1.0/tests/test_keys.py +79 -0
  77. agent_once-0.1.0/tests/test_otel.py +230 -0
  78. agent_once-0.1.0/tests/test_postgres.py +225 -0
  79. agent_once-0.1.0/tests/test_regressions.py +325 -0
  80. agent_once-0.1.0/tests/test_stateful.py +206 -0
  81. agent_once-0.1.0/tests/test_stress.py +89 -0
  82. agent_once-0.1.0/tests/test_timeout.py +108 -0
  83. agent_once-0.1.0/tests/test_ttl.py +513 -0
  84. agent_once-0.1.0/tests/test_verifiers_http.py +198 -0
  85. agent_once-0.1.0/tests/test_verifiers_stripe.py +450 -0
  86. agent_once-0.1.0/tests/test_verify.py +195 -0
@@ -0,0 +1,119 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ lint:
9
+ runs-on: ubuntu-latest
10
+ timeout-minutes: 10
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install -e ".[dev,postgres,stripe]" mcp langchain-core langgraph
17
+ - run: ruff check .
18
+ - run: ruff format --check .
19
+ - run: mypy
20
+
21
+ # Linux runs every test against both SQLite and a Postgres service container.
22
+ test:
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 15
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
29
+ services:
30
+ postgres:
31
+ image: postgres:17
32
+ env:
33
+ POSTGRES_PASSWORD: postgres
34
+ POSTGRES_DB: once_test
35
+ ports:
36
+ - 5432:5432
37
+ options: >-
38
+ --health-cmd "pg_isready -U postgres"
39
+ --health-interval 2s
40
+ --health-timeout 5s
41
+ --health-retries 30
42
+ env:
43
+ ONCE_TEST_POSTGRES_DSN: postgresql://postgres:postgres@localhost:5432/once_test
44
+ steps:
45
+ - uses: actions/checkout@v4
46
+ - uses: actions/setup-python@v5
47
+ with:
48
+ python-version: ${{ matrix.python-version }}
49
+ - run: pip install -e ".[dev,postgres]"
50
+ - run: pytest -q --cov=once --cov-report=term-missing
51
+ - if: matrix.python-version == '3.12'
52
+ run: pytest -q -m heavy tests/test_stress.py
53
+
54
+ # Service containers only run on Linux, so these skip the Postgres tests and
55
+ # leave postgres.py out of the coverage floor.
56
+ test-other:
57
+ runs-on: ${{ matrix.os }}
58
+ timeout-minutes: 15
59
+ strategy:
60
+ fail-fast: false
61
+ matrix:
62
+ os: [windows-latest, macos-latest]
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+ - uses: actions/setup-python@v5
66
+ with:
67
+ python-version: "3.12"
68
+ - run: pip install -e ".[dev]"
69
+ - run: pytest -q --cov=once --cov-fail-under=0
70
+ - run: coverage report --omit="*postgres.py" --fail-under=95 --show-missing
71
+
72
+ integrations:
73
+ runs-on: ubuntu-latest
74
+ timeout-minutes: 15
75
+ strategy:
76
+ fail-fast: false
77
+ matrix:
78
+ mcp: ["mcp<2", "mcp>=2"]
79
+ steps:
80
+ - uses: actions/checkout@v4
81
+ - uses: actions/setup-python@v5
82
+ with:
83
+ python-version: "3.12"
84
+ - run: pip install -e ".[dev,stripe]" langchain-core langgraph "${{ matrix.mcp }}"
85
+ - run: pytest -q tests/test_integrations.py tests/test_integrations_mcp.py tests/test_integrations_langchain.py tests/test_integrations_stripe.py
86
+ - run: python examples/stripe_example.py
87
+ - run: python examples/langgraph_example.py
88
+ - run: python examples/mcp_server.py --demo
89
+ - run: pytest -q -m heavy tests/test_stress.py
90
+
91
+ # One environment per framework, since their pins conflict (crewai wants openai<3).
92
+ adapters:
93
+ runs-on: ubuntu-latest
94
+ timeout-minutes: 15
95
+ strategy:
96
+ fail-fast: false
97
+ matrix:
98
+ include:
99
+ - framework: openai_agents
100
+ extra: openai-agents
101
+ - framework: pydantic_ai
102
+ extra: pydantic-ai
103
+ - framework: crewai
104
+ extra: crewai
105
+ env:
106
+ CREWAI_DISABLE_TELEMETRY: "true"
107
+ OTEL_SDK_DISABLED: "true"
108
+ PYDANTIC_AI_NO_BANNER: "1"
109
+ steps:
110
+ - uses: actions/checkout@v4
111
+ - uses: actions/setup-python@v5
112
+ with:
113
+ python-version: "3.12"
114
+ - run: pip install -e ".[dev,${{ matrix.extra }}]"
115
+ - run: python examples/${{ matrix.framework }}_example.py
116
+ - run: pytest -q tests/test_integrations_${{ matrix.framework }}.py tests/test_integrations_adapters.py
117
+ - run: >-
118
+ mypy --python-version 3.12 src/once/integrations/${{ matrix.framework }}.py
119
+ tests/test_integrations_${{ matrix.framework }}.py examples/${{ matrix.framework }}_example.py
@@ -0,0 +1,57 @@
1
+ name: Docs
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: docs-${{ github.ref }}
14
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+ - run: pip install -r requirements-docs.txt
26
+ - run: mkdocs build --strict
27
+ - if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
28
+ uses: actions/upload-pages-artifact@v3
29
+ with:
30
+ path: site
31
+
32
+ deploy:
33
+ if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ timeout-minutes: 10
37
+ permissions:
38
+ pages: write
39
+ id-token: write
40
+ environment:
41
+ name: github-pages
42
+ url: ${{ steps.deployment.outputs.page_url }}
43
+ steps:
44
+ # Skip, don't fail, until Settings > Pages > Source is set to GitHub Actions.
45
+ - id: pages
46
+ env:
47
+ GH_TOKEN: ${{ github.token }}
48
+ run: |
49
+ if gh api "repos/$GITHUB_REPOSITORY/pages" > /dev/null 2>&1; then
50
+ echo "enabled=true" >> "$GITHUB_OUTPUT"
51
+ else
52
+ echo "::notice::GitHub Pages is not enabled, skipping deploy"
53
+ echo "enabled=false" >> "$GITHUB_OUTPUT"
54
+ fi
55
+ - id: deployment
56
+ if: steps.pages.outputs.enabled == 'true'
57
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,42 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ timeout-minutes: 10
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+ - run: pip install build twine
21
+ - run: python -m build
22
+ - run: twine check --strict dist/*
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ timeout-minutes: 10
32
+ environment:
33
+ name: pypi
34
+ url: https://pypi.org/p/agent-once
35
+ permissions:
36
+ id-token: write
37
+ steps:
38
+ - uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.db
11
+ *.db-wal
12
+ *.db-shm
13
+ .coverage
14
+ coverage.xml
15
+ htmlcov/
16
+ site/
17
+ .hypothesis/
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ First release.
6
+
7
+ - `Once` with `@once.tool`, `execute` and `aexecute`, backed by SQLite.
8
+ - Records move through pending, done, failed and unknown. Failed and unknown are never re-run on their own.
9
+ - `verify` callbacks and `stale_after` to resolve unknown and abandoned pending records.
10
+ - Async tools, `timeout=`, cancellation marks unknown. Sync and async callers in one process wait for each other.
11
+ - `once` command line tool: `ls`, `show`, `reset`, `gc`. `Once.records()` and `Once.purge()`.
12
+ - `Once.close()`, and `Once` works as a context manager.
13
+ - `exclude=` on `once.tool` to leave parameters out of the key.
14
+ - `once.integrations.mcp` and `once.integrations.langchain` decorators, with examples.
15
+ - `once.integrations.openai_agents`, `once.integrations.pydantic_ai` and `once.integrations.crewai` decorators, with examples and extras. The `crewai` and `openai-agents` extras can't be installed together. Pydantic AI tools take their scope from `scope_arg`, then `scope_from(ctx)`, then the enclosing `once.scope()` (read with `current_scope()`), then `ctx.conversation_id`, or `ctx.run_id` with `scope="run"`.
16
+ - The framework helpers take `ttl=`.
17
+ - `PostgresStore` for workers on several machines, behind the `postgres` extra. Use `Once("postgresql://...")` or `Once(store=PostgresStore(dsn, table=...))`. The CLI accepts a Postgres URL for `--db` and has a `--table` option.
18
+ - `Store` protocol, so `Once(store=...)` takes any backend that implements it.
19
+ - `once.verifiers`: `stripe_verifier` and `stripe_params` for Stripe charges, PaymentIntents and refunds (`stripe` extra, or pass a client), and `http_verifier` for any API that can look an effect up by key. `stripe_verifier` falls back to listing when search isn't available to the account.
20
+ - `ttl=` on `Once`, `@once.tool`, `execute` and `aexecute`. Done records expire and the next call runs again; failed and unknown never expire. Stored in a new `expires_at` column, added to existing SQLite and Postgres tables on open. `once gc --expired` and `Once.purge(expired=True)` delete expired records.
21
+ - `current_scope()` returns the active `once.scope()` value. An `idempotency_key` parameter that gets `""` or `None` now means "no key", so one is computed. A `""` default used to become the key itself, so two tools with that default hit `KeyConflict`.
22
+ - `expires_at` is indexed in both stores, so `gc --expired` doesn't scan the whole table. The index is added to existing tables on open.
23
+ - `Once(hooks=Hooks(...))` with `on_claim`, `on_done`, `on_failed`, `on_unknown` and `on_replay`. Hooks get an `Event`, may be async under `aexecute`, and an error in one is logged, not raised.
24
+ - OpenTelemetry spans (`once <tool_name>`) behind the `otel` extra. No-op without a configured tracer.
25
+
26
+ Fixes from a review before the release:
27
+
28
+ - A sync call with `timeout=` runs in a thread that keeps the caller's context, so `once.scope()` and tracing still apply inside it.
29
+ - A `ttl` that would expire after year 9999 is treated as never expiring instead of raising `OverflowError`.
30
+ - A claim is given back if the call is interrupted (or an async store step is cancelled) before the tool starts, so the key isn't left pending.
31
+ - Storing the outcome is retried once after a store error, so a dropped connection doesn't leave a finished call pending.
32
+ - Calling the same key from inside its own call raises `OnceError` instead of deadlocking. This includes a sync call inside an async owner.
33
+ - `exclude=` names that are not parameters of the function raise an error.
34
+ - `EffectUnknown` is raised even if the record was reset while the call was finishing.
35
+ - `stripe_verifier` reads the status of the object it finds: failed or canceled counts as not happened, and a status still in flux raises `StripeInconclusive` so the record stays unknown. It also stays unknown when the list scan runs out before the end.
36
+ - `http_verifier` JSON-encodes non-string arguments in the URL, and rejects an argument named `key` that would clash with `{key}`.
37
+ - `PostgresStore` takes `statement_timeout` (30 seconds by default) and retries a statement once after a dropped connection. Postgres URLs are accepted in any case and with `+psycopg`.
38
+ - The CLI refuses SQLite files without an effects table and rejects a negative `--limit`.
39
+ - JSON errors name the argument or the verify function that couldn't be serialized.
40
+ - README and docs have a Gotchas section.
41
+
42
+ Changes made before the release that break keys from earlier development builds:
43
+
44
+ - `make_key` is now `sha256(canonical_json([tool_name, args, scope]))`. It used to concatenate the parts, which let some tool names and scopes collide.
45
+ - `canonical_json` now escapes non-ASCII characters (`ensure_ascii=True`), so strings with lone surrogates can be hashed.
@@ -0,0 +1,36 @@
1
+ # Contributing
2
+
3
+ Bug reports and pull requests are welcome. For anything bigger than a small fix, open an issue first so we can agree on the approach.
4
+
5
+ ## Setup
6
+
7
+ ```
8
+ python -m venv .venv
9
+ .venv/bin/pip install -e '.[dev,postgres,stripe]'
10
+ ```
11
+
12
+ ## Checks
13
+
14
+ CI runs these, so run them before opening a pull request:
15
+
16
+ ```
17
+ .venv/bin/ruff format --check .
18
+ .venv/bin/ruff check .
19
+ .venv/bin/mypy src tests
20
+ .venv/bin/python -m pytest
21
+ ```
22
+
23
+ The Postgres tests are skipped unless `ONCE_TEST_POSTGRES_DSN` points at a database they can create tables in, for example `postgresql://postgres:postgres@localhost:5432/once_test`. Slow stress tests are marked `heavy` and run with `pytest -m heavy`.
24
+
25
+ The docs site is MkDocs Material:
26
+
27
+ ```
28
+ pip install -r requirements-docs.txt
29
+ mkdocs serve
30
+ ```
31
+
32
+ ## Pull requests
33
+
34
+ - Add a test for a bug fix or new behavior.
35
+ - Update the README, the docs and `CHANGELOG.md` when behavior changes.
36
+ - Keep changes to stored keys and the table schema backward compatible, or say clearly in the changelog that they are not.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 frogscooper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.