keycardai-temporal 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.
@@ -0,0 +1,194 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ .venv-leg
126
+ env/
127
+ venv/
128
+ ENV/
129
+ env.bak/
130
+ venv.bak/
131
+
132
+ # Spyder project settings
133
+ .spyderproject
134
+ .spyproject
135
+
136
+ # Rope project settings
137
+ .ropeproject
138
+
139
+ # mkdocs documentation
140
+ /site
141
+
142
+ # mypy
143
+ .mypy_cache/
144
+ .dmypy.json
145
+ dmypy.json
146
+
147
+ # Pyre type checker
148
+ .pyre/
149
+
150
+ # pytype static type analyzer
151
+ .pytype/
152
+
153
+ # Cython debug symbols
154
+ cython_debug/
155
+
156
+ # PyCharm
157
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158
+ # be added to the global gitignore or merged into this project gitignore. For a PyCharm
159
+ # project, uncomment below!
160
+ #.idea/
161
+
162
+ # VS Code
163
+ .vscode/
164
+
165
+ # OS generated files
166
+ .DS_Store
167
+ .DS_Store?
168
+ ._*
169
+ .Spotlight-V100
170
+ .Trashes
171
+ ehthumbs.db
172
+ Thumbs.db
173
+
174
+ # Backup files
175
+ *~
176
+ *.bak
177
+ *.backup
178
+ *.old
179
+ *.orig
180
+
181
+ # Temporary files
182
+ *.tmp
183
+ *.temp
184
+
185
+ # IDE files
186
+ *.swp
187
+ *.swo
188
+ *~
189
+
190
+ # Local development
191
+ .local/
192
+
193
+ # Claude Code
194
+ CLAUDE.md
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.5
2
+ Name: keycardai-temporal
3
+ Version: 0.1.0
4
+ Summary: Temporal integration for Keycard: per-call token minting for activities, with nothing in workflow history
5
+ Project-URL: Homepage, https://github.com/keycardai/python-sdk
6
+ Project-URL: Repository, https://github.com/keycardai/python-sdk
7
+ Project-URL: Documentation, https://docs.keycardai.com
8
+ Project-URL: Issues, https://github.com/keycardai/python-sdk/issues
9
+ Author-email: Keycard <support@keycard.ai>
10
+ License: MIT
11
+ Keywords: activities,authentication,keycard,oauth,temporal,token-exchange,workflows
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: keycardai-oauth>=0.26.0
25
+ Requires-Dist: temporalio>=1.32.0
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest-asyncio>=1.1.0; extra == 'test'
28
+ Requires-Dist: pytest>=8.4.1; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # keycardai-temporal
32
+
33
+ Per-call Keycard token minting for Temporal Python workers, built on `keycardai-oauth`.
34
+
35
+ An activity declares the resource it needs with `@grant(resource)`, the worker's `KeycardInterceptor` mints a fresh token for every activity execution, and `access()` returns it inside the activity. Nothing is written to workflow history.
36
+
37
+ ```bash
38
+ pip install keycardai-temporal
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ from datetime import timedelta
45
+
46
+ from temporalio import activity, workflow
47
+ from temporalio.worker import Worker
48
+
49
+ from keycardai.temporal import KeycardInterceptor, access, grant
50
+
51
+ LEDGER = "https://ledger.example.com"
52
+
53
+
54
+ @grant(LEDGER)
55
+ @activity.defn
56
+ async def post_entry(order_id: str) -> str:
57
+ token = access().access_token # fresh for this execution only
58
+ ... # call the ledger with the token
59
+ return "posted"
60
+
61
+
62
+ @workflow.defn
63
+ class SettlementWorkflow:
64
+ @workflow.run
65
+ async def run(self, order_id: str) -> str:
66
+ return await workflow.execute_activity(
67
+ post_entry, order_id, start_to_close_timeout=timedelta(seconds=30)
68
+ )
69
+
70
+
71
+ async def main(client):
72
+ interceptor = KeycardInterceptor("https://<zone-id>.keycard.cloud")
73
+ async with Worker(
74
+ client,
75
+ task_queue="settlement",
76
+ workflows=[SettlementWorkflow],
77
+ activities=[post_entry],
78
+ interceptors=[interceptor],
79
+ ):
80
+ ...
81
+ ```
82
+
83
+ ### Credential discovery
84
+
85
+ With no `credential` argument, `KeycardInterceptor` calls `keycardai.oauth.server.discover_credential()`, the SDK-wide environment convention:
86
+
87
+ - `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET` together build a `ClientSecret`.
88
+ - A token file named by `KEYCARD_EKS_WORKLOAD_IDENTITY_TOKEN_FILE`, `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE`, `AWS_WEB_IDENTITY_TOKEN_FILE`, or `AZURE_FEDERATED_TOKEN_FILE` builds a `WorkloadIdentity`.
89
+ - `KEYCARD_APPLICATION_CREDENTIAL_TYPE` (`client_secret` or `workload_identity`; `eks_workload_identity` is a legacy alias) names the type to use, and wins over everything else in the environment.
90
+
91
+ When the environment can build more than one credential and the type variable does not choose between them, the worker fails at startup with `GrantConfigurationError` instead of guessing. EKS IRSA injects `AWS_WEB_IDENTITY_TOKEN_FILE` into pods automatically, so a worker meant to use a client secret on EKS must set `KEYCARD_APPLICATION_CREDENTIAL_TYPE=client_secret`. Any `keycardai.oauth.server.ApplicationCredential` can also be passed explicitly, which skips discovery entirely.
92
+
93
+ ## Keycard setup
94
+
95
+ In your Keycard zone, once:
96
+
97
+ 1. Create an application for the worker with a credential (a `ClientSecret`, or any workload identity credential the worker's platform supports).
98
+ 2. Create the resource the activities will call. For plain client-credentials issuance, the Zone Provider is enough as its credential provider: nothing exchanges from its tokens.
99
+ 3. Add the resource to the application's dependencies. App-only issuance needs the dependency; with no user there is no consent step.
100
+ 4. For on-behalf-of activities, one more piece of zone topology: the exchange rule requires the exchanging application to provide the resource the subject token is audienced to, so the worker's application needs a small anchor resource of its own, with user sessions audienced to it.
101
+
102
+ The resource identifier in `@grant(...)` must match the console registration byte for byte; a trailing character difference reads as a different resource and policy denies it.
103
+
104
+ ## Identity modes
105
+
106
+ - `@grant(resource)`: the application acts as itself (client credentials). Requires a `ClientSecret` credential.
107
+ - `@grant(resource, subject_from=...)`: the application acts on behalf of a user. The activity input carries an identity reference (a user id, never a token). The interceptor's `subject_token_provider`, an application-supplied session lookup, returns that user's current session token, and an RFC 8693 exchange turns it into a token for the resource.
108
+ - `@grant(resource, subject_from=..., impersonate=True)`: impersonation, for workflows that outlive the user's session. The located value is a stable user identifier (email or oid) sent directly to the zone, which mints a short-lived substitute-user token. No session lookup runs and no `subject_token_provider` is needed. This is a different trust model from delegation: the worker asserts who the user is, and zone policy is the control. It requires a confidential client, application consent set to implicit, the resource declared as a dependency of the application, a prior delegated grant established by the user for the resource, and zone policy that explicitly permits the application to impersonate (forbidden by default). Prefer live delegation whenever the user's session is still expected to exist.
109
+
110
+ ### Locating the identity reference
111
+
112
+ ```python
113
+ from typing import Annotated
114
+ from dataclasses import dataclass
115
+
116
+ from keycardai.temporal import Subject, grant
117
+
118
+
119
+ @dataclass
120
+ class Order:
121
+ order_id: str
122
+ approver_id: Annotated[str, Subject()]
123
+
124
+
125
+ @grant(LEDGER) # Subject() marker, validated at decoration time
126
+ async def approve(order: Order) -> None: ...
127
+
128
+ @grant(LEDGER, subject_from="approver_id") # parameter name ...
129
+ async def approve(order_id: str, approver_id: str) -> None: ...
130
+
131
+ @grant(LEDGER, subject_from="order.approver_id") # ... or a dotted path into one
132
+ async def approve(order: dict) -> None: ...
133
+
134
+ @grant(LEDGER, subject_from=lambda order: order["approver_id"]) # sync callable escape hatch
135
+ async def approve(order: dict) -> None: ...
136
+ ```
137
+
138
+ Use one strategy per activity: a `Subject()` marker together with `subject_from` is rejected at decoration time.
139
+
140
+ The worker supplies the session lookup:
141
+
142
+ ```python
143
+ async def session_token_for(approver_id: str) -> str:
144
+ return await sessions.current_token(approver_id)
145
+
146
+ interceptor = KeycardInterceptor(
147
+ "https://<zone-id>.keycard.cloud",
148
+ subject_token_provider=session_token_for,
149
+ )
150
+ ```
151
+
152
+ ## Design notes
153
+
154
+ - Tokens never touch durable state. Workflow history is replayable and permanent, so unlike header-based context-propagation interceptors, nothing is written to activity headers, arguments, or return values. The token exists only inside one execution's context. This is also why on-behalf-of activities receive an identity reference instead of a token: the session lookup and exchange happen at the edge, inside the execution, so a session revoked mid-workflow is never replayed from state.
155
+ - A mint failure raises before the activity body runs, and there is no fallback path. Tokens live in the SDK's shared `AccessContext` (the same container `keycardai-mcp` uses), but where that idiom is non-throwing, the interceptor converts recorded errors into raises on purpose: in Temporal, raising is the error channel.
156
+ - Transient mint failures are retryable; permanent denials are not. Network failures and unclassified errors let the activity retry policy govern what happens next, while `access_denied`, `insufficient_authorization`, and `invalid_client` raise `ApplicationError(type="KeycardAccessDenied", non_retryable=True)` immediately. Misdeclarations surface as `GrantConfigurationError`, retryable by default so a worker redeploy with the fix lets the next retry succeed; list `"GrantConfigurationError"` in the retry policy's `non_retryable_error_types` to give up sooner.
157
+ - No token caching, per Keycard's credential rules; per-call mint is the contract. One OAuth client is created per worker and reused; only the tokens are fresh.
158
+ - The package wraps its own `keycardai` imports in `workflow.unsafe.imports_passed_through()`, the idiom from Temporal's sentry sample, so consumers import it normally even in files that define workflows.
159
+ - Works with async activities and with sync activities on the thread-pool executor (the Temporal SDK copies contextvars into the thread). Sync activities on a process-pool executor are not supported because contextvars do not cross processes.
160
+
161
+ ## Tests
162
+
163
+ ```bash
164
+ cd packages/temporal && uv run --extra test pytest tests/ -v
165
+ ```
166
+
167
+ `tests/test_interceptor.py` drives the interceptor chain directly with the OAuth client stubbed. `tests/test_history_hygiene.py` runs a real workflow against a local Temporal dev server (downloaded by `temporalio` on first use), then scans the recorded history, including base64-decoded payloads, and asserts the minted token appears nowhere. Neither needs a Keycard zone.
@@ -0,0 +1,137 @@
1
+ # keycardai-temporal
2
+
3
+ Per-call Keycard token minting for Temporal Python workers, built on `keycardai-oauth`.
4
+
5
+ An activity declares the resource it needs with `@grant(resource)`, the worker's `KeycardInterceptor` mints a fresh token for every activity execution, and `access()` returns it inside the activity. Nothing is written to workflow history.
6
+
7
+ ```bash
8
+ pip install keycardai-temporal
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from datetime import timedelta
15
+
16
+ from temporalio import activity, workflow
17
+ from temporalio.worker import Worker
18
+
19
+ from keycardai.temporal import KeycardInterceptor, access, grant
20
+
21
+ LEDGER = "https://ledger.example.com"
22
+
23
+
24
+ @grant(LEDGER)
25
+ @activity.defn
26
+ async def post_entry(order_id: str) -> str:
27
+ token = access().access_token # fresh for this execution only
28
+ ... # call the ledger with the token
29
+ return "posted"
30
+
31
+
32
+ @workflow.defn
33
+ class SettlementWorkflow:
34
+ @workflow.run
35
+ async def run(self, order_id: str) -> str:
36
+ return await workflow.execute_activity(
37
+ post_entry, order_id, start_to_close_timeout=timedelta(seconds=30)
38
+ )
39
+
40
+
41
+ async def main(client):
42
+ interceptor = KeycardInterceptor("https://<zone-id>.keycard.cloud")
43
+ async with Worker(
44
+ client,
45
+ task_queue="settlement",
46
+ workflows=[SettlementWorkflow],
47
+ activities=[post_entry],
48
+ interceptors=[interceptor],
49
+ ):
50
+ ...
51
+ ```
52
+
53
+ ### Credential discovery
54
+
55
+ With no `credential` argument, `KeycardInterceptor` calls `keycardai.oauth.server.discover_credential()`, the SDK-wide environment convention:
56
+
57
+ - `KEYCARD_CLIENT_ID` and `KEYCARD_CLIENT_SECRET` together build a `ClientSecret`.
58
+ - A token file named by `KEYCARD_EKS_WORKLOAD_IDENTITY_TOKEN_FILE`, `AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE`, `AWS_WEB_IDENTITY_TOKEN_FILE`, or `AZURE_FEDERATED_TOKEN_FILE` builds a `WorkloadIdentity`.
59
+ - `KEYCARD_APPLICATION_CREDENTIAL_TYPE` (`client_secret` or `workload_identity`; `eks_workload_identity` is a legacy alias) names the type to use, and wins over everything else in the environment.
60
+
61
+ When the environment can build more than one credential and the type variable does not choose between them, the worker fails at startup with `GrantConfigurationError` instead of guessing. EKS IRSA injects `AWS_WEB_IDENTITY_TOKEN_FILE` into pods automatically, so a worker meant to use a client secret on EKS must set `KEYCARD_APPLICATION_CREDENTIAL_TYPE=client_secret`. Any `keycardai.oauth.server.ApplicationCredential` can also be passed explicitly, which skips discovery entirely.
62
+
63
+ ## Keycard setup
64
+
65
+ In your Keycard zone, once:
66
+
67
+ 1. Create an application for the worker with a credential (a `ClientSecret`, or any workload identity credential the worker's platform supports).
68
+ 2. Create the resource the activities will call. For plain client-credentials issuance, the Zone Provider is enough as its credential provider: nothing exchanges from its tokens.
69
+ 3. Add the resource to the application's dependencies. App-only issuance needs the dependency; with no user there is no consent step.
70
+ 4. For on-behalf-of activities, one more piece of zone topology: the exchange rule requires the exchanging application to provide the resource the subject token is audienced to, so the worker's application needs a small anchor resource of its own, with user sessions audienced to it.
71
+
72
+ The resource identifier in `@grant(...)` must match the console registration byte for byte; a trailing character difference reads as a different resource and policy denies it.
73
+
74
+ ## Identity modes
75
+
76
+ - `@grant(resource)`: the application acts as itself (client credentials). Requires a `ClientSecret` credential.
77
+ - `@grant(resource, subject_from=...)`: the application acts on behalf of a user. The activity input carries an identity reference (a user id, never a token). The interceptor's `subject_token_provider`, an application-supplied session lookup, returns that user's current session token, and an RFC 8693 exchange turns it into a token for the resource.
78
+ - `@grant(resource, subject_from=..., impersonate=True)`: impersonation, for workflows that outlive the user's session. The located value is a stable user identifier (email or oid) sent directly to the zone, which mints a short-lived substitute-user token. No session lookup runs and no `subject_token_provider` is needed. This is a different trust model from delegation: the worker asserts who the user is, and zone policy is the control. It requires a confidential client, application consent set to implicit, the resource declared as a dependency of the application, a prior delegated grant established by the user for the resource, and zone policy that explicitly permits the application to impersonate (forbidden by default). Prefer live delegation whenever the user's session is still expected to exist.
79
+
80
+ ### Locating the identity reference
81
+
82
+ ```python
83
+ from typing import Annotated
84
+ from dataclasses import dataclass
85
+
86
+ from keycardai.temporal import Subject, grant
87
+
88
+
89
+ @dataclass
90
+ class Order:
91
+ order_id: str
92
+ approver_id: Annotated[str, Subject()]
93
+
94
+
95
+ @grant(LEDGER) # Subject() marker, validated at decoration time
96
+ async def approve(order: Order) -> None: ...
97
+
98
+ @grant(LEDGER, subject_from="approver_id") # parameter name ...
99
+ async def approve(order_id: str, approver_id: str) -> None: ...
100
+
101
+ @grant(LEDGER, subject_from="order.approver_id") # ... or a dotted path into one
102
+ async def approve(order: dict) -> None: ...
103
+
104
+ @grant(LEDGER, subject_from=lambda order: order["approver_id"]) # sync callable escape hatch
105
+ async def approve(order: dict) -> None: ...
106
+ ```
107
+
108
+ Use one strategy per activity: a `Subject()` marker together with `subject_from` is rejected at decoration time.
109
+
110
+ The worker supplies the session lookup:
111
+
112
+ ```python
113
+ async def session_token_for(approver_id: str) -> str:
114
+ return await sessions.current_token(approver_id)
115
+
116
+ interceptor = KeycardInterceptor(
117
+ "https://<zone-id>.keycard.cloud",
118
+ subject_token_provider=session_token_for,
119
+ )
120
+ ```
121
+
122
+ ## Design notes
123
+
124
+ - Tokens never touch durable state. Workflow history is replayable and permanent, so unlike header-based context-propagation interceptors, nothing is written to activity headers, arguments, or return values. The token exists only inside one execution's context. This is also why on-behalf-of activities receive an identity reference instead of a token: the session lookup and exchange happen at the edge, inside the execution, so a session revoked mid-workflow is never replayed from state.
125
+ - A mint failure raises before the activity body runs, and there is no fallback path. Tokens live in the SDK's shared `AccessContext` (the same container `keycardai-mcp` uses), but where that idiom is non-throwing, the interceptor converts recorded errors into raises on purpose: in Temporal, raising is the error channel.
126
+ - Transient mint failures are retryable; permanent denials are not. Network failures and unclassified errors let the activity retry policy govern what happens next, while `access_denied`, `insufficient_authorization`, and `invalid_client` raise `ApplicationError(type="KeycardAccessDenied", non_retryable=True)` immediately. Misdeclarations surface as `GrantConfigurationError`, retryable by default so a worker redeploy with the fix lets the next retry succeed; list `"GrantConfigurationError"` in the retry policy's `non_retryable_error_types` to give up sooner.
127
+ - No token caching, per Keycard's credential rules; per-call mint is the contract. One OAuth client is created per worker and reused; only the tokens are fresh.
128
+ - The package wraps its own `keycardai` imports in `workflow.unsafe.imports_passed_through()`, the idiom from Temporal's sentry sample, so consumers import it normally even in files that define workflows.
129
+ - Works with async activities and with sync activities on the thread-pool executor (the Temporal SDK copies contextvars into the thread). Sync activities on a process-pool executor are not supported because contextvars do not cross processes.
130
+
131
+ ## Tests
132
+
133
+ ```bash
134
+ cd packages/temporal && uv run --extra test pytest tests/ -v
135
+ ```
136
+
137
+ `tests/test_interceptor.py` drives the interceptor chain directly with the OAuth client stubbed. `tests/test_history_hygiene.py` runs a real workflow against a local Temporal dev server (downloaded by `temporalio` on first use), then scans the recorded history, including base64-decoded payloads, and asserts the minted token appears nowhere. Neither needs a Keycard zone.
@@ -0,0 +1,141 @@
1
+ [project]
2
+ name = "keycardai-temporal"
3
+ dynamic = ["version"]
4
+ description = "Temporal integration for Keycard: per-call token minting for activities, with nothing in workflow history"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Keycard", email = "support@keycard.ai" }]
9
+ dependencies = [
10
+ "keycardai-oauth>=0.26.0",
11
+ "temporalio>=1.32.0",
12
+ ]
13
+ keywords = ["temporal", "workflows", "activities", "oauth", "token-exchange", "authentication", "keycard"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Operating System :: OS Independent",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Topic :: Security",
25
+ "License :: OSI Approved :: MIT License",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ test = [
30
+ "pytest>=8.4.1",
31
+ "pytest-asyncio>=1.1.0",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/keycardai/python-sdk"
36
+ Repository = "https://github.com/keycardai/python-sdk"
37
+ Documentation = "https://docs.keycardai.com"
38
+ Issues = "https://github.com/keycardai/python-sdk/issues"
39
+
40
+ [build-system]
41
+ requires = ["hatchling", "uv-dynamic-versioning"]
42
+ build-backend = "hatchling.build"
43
+
44
+ [tool.hatch.version]
45
+ source = "uv-dynamic-versioning"
46
+
47
+ [tool.uv-dynamic-versioning]
48
+ vcs = "git"
49
+ pattern = "(?P<base>\\d+\\.\\d+\\.\\d+)-keycardai-temporal"
50
+ style = "pep440"
51
+
52
+ [[tool.uv.index]]
53
+ name = "testpypi"
54
+ url = "https://test.pypi.org/simple/"
55
+ publish-url = "https://test.pypi.org/legacy/"
56
+ explicit = true
57
+
58
+ [tool.hatch.build.targets.wheel]
59
+ packages = ["src/keycardai"]
60
+
61
+ [tool.ruff]
62
+ line-length = 88
63
+ target-version = "py310"
64
+
65
+ [tool.ruff.lint]
66
+ select = [
67
+ "E", # pycodestyle errors
68
+ "W", # pycodestyle warnings
69
+ "F", # pyflakes
70
+ "I", # isort
71
+ "B", # flake8-bugbear
72
+ "C4", # flake8-comprehensions
73
+ "UP", # pyupgrade
74
+ ]
75
+ ignore = [
76
+ "E501", # line too long, we'll handle case by case
77
+ ]
78
+ isort = { combine-as-imports = true, known-first-party = ["keycardai"] }
79
+
80
+ [tool.ruff.lint.per-file-ignores]
81
+ "tests/**/*.py" = ["T20"]
82
+
83
+ [tool.mypy]
84
+ strict = true
85
+ disallow_incomplete_defs = false
86
+ disallow_untyped_defs = false
87
+ disallow_untyped_calls = false
88
+
89
+ [[tool.mypy.overrides]]
90
+ module = "tests.*"
91
+ disallow_untyped_defs = false
92
+
93
+ [tool.coverage.run]
94
+ source = ["tests", "src/keycardai"]
95
+
96
+ [tool.coverage.report]
97
+ show_missing = true
98
+ exclude_also = [
99
+ "pragma: no cover",
100
+ "if TYPE_CHECKING:",
101
+ "@abc.abstractmethod",
102
+ "raise NotImplementedError",
103
+ ]
104
+
105
+ [tool.pytest.ini_options]
106
+ testpaths = ["tests"]
107
+ addopts = "-ra -q"
108
+ asyncio_mode = "auto"
109
+
110
+ [tool.commitizen]
111
+ name = "cz_customize"
112
+ version = "0.1.0"
113
+ tag_format = "${version}-keycardai-temporal"
114
+ ignored_tag_formats = ["${version}-*"]
115
+ update_changelog_on_bump = true
116
+ bump_message = "bump: keycardai-temporal $current_version → $new_version"
117
+ major_version_zero = true
118
+
119
+ [tool.commitizen.customize]
120
+ # Scoped bump detection: without bump_pattern/bump_map commitizen falls back to
121
+ # its default classifier, which matches any `feat(...)!:` regardless of scope.
122
+ changelog_pattern = "^(feat|fix|refactor|perf|test|revert)\\(keycardai-temporal\\)(!)?:"
123
+ bump_pattern = "^((?:feat|fix|refactor|perf|revert|BREAKING[\\- ]CHANGE)\\(keycardai-temporal\\)!?):"
124
+
125
+ [tool.commitizen.customize.bump_map]
126
+ "^.+\\(keycardai-temporal\\)!$" = "MAJOR"
127
+ "^BREAKING[\\- ]CHANGE" = "MAJOR"
128
+ "^feat" = "MINOR"
129
+ "^fix" = "PATCH"
130
+ "^refactor" = "PATCH"
131
+ "^perf" = "PATCH"
132
+ "^revert" = "PATCH"
133
+
134
+ [tool.commitizen.customize.bump_map_major_version_zero]
135
+ "^.+\\(keycardai-temporal\\)!$" = "MINOR"
136
+ "^BREAKING[\\- ]CHANGE" = "MINOR"
137
+ "^feat" = "MINOR"
138
+ "^fix" = "PATCH"
139
+ "^refactor" = "PATCH"
140
+ "^perf" = "PATCH"
141
+ "^revert" = "PATCH"