-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathpyproject.toml
More file actions
363 lines (341 loc) · 17.4 KB
/
Copy pathpyproject.toml
File metadata and controls
363 lines (341 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
[build-system]
# Must allow the uv version python-sdk-publish.yml's build job pins, so releases
# build with that uv's built-in backend. Bumped by hand with it; Dependabot
# ignores uv_build (see .github/dependabot.yml).
requires = ["uv_build>=0.12.17,<0.13"]
build-backend = "uv_build"
[project]
name = "permit"
version = "3.0.0"
description = "Permit.io python sdk"
readme = "README.md"
requires-python = ">=3.10"
license = "Apache-2.0"
license-files = ["LICENSE"]
authors = [{ name = "Permit.io", email = "support@permit.io" }]
classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
# Open ranges on purpose: this is a library, and consumers resolve these
# against their own tree. The floors are the real exposure, which is why
# .github/scripts/audit-deps.sh scans them with --resolution lowest-direct.
dependencies = [
"aiohttp>=3.14.3,<4",
# 0.7.3 is the first loguru release that imports without a DeprecationWarning on
# Python 3.14: earlier ones call asyncio.iscoroutinefunction, which 3.16 removes.
"loguru>=0.7.3,<1",
# pydantic has one line per Python range, updated by hand: Dependabot ignores
# pydantic (see .github/dependabot.yml). Why each version is excluded:
# - CVE-2024-3772 (ReDoS in email validation) affects pydantic 1.x before
# 1.10.13. Under pydantic 2, permit validates emails with the pydantic.v1 copy
# that pydantic 2 bundles, and only 2.4.2 and later bundle the fixed 1.10.13:
# 2.0.1 bundles 1.10.11, and 2.4.0 and 2.4.1 bundle 1.10.12. So 2.0-2.3, 2.4.0
# and 2.4.1 are excluded on every Python.
# - pydantic 2.0 also fails every API call that parses a response: its
# pydantic.v1.parse_obj_as builds the model with pydantic 2, which rejects the
# `__root__` field.
# - Below Python 3.14 the pydantic 1 floor is 1.10.18. Type checkers read
# permit's models from the pydantic.v1 package, which pydantic 1 first ships
# in 1.10.17, and 1.10.13-1.10.17 emit about 2,400 DeprecationWarnings on
# `import permit` under Python 3.13 (typing._eval_type called without
# type_params).
# - On Python 3.13 the pydantic 2 floor is 2.8.0: 2.4.2-2.7.x require a
# pydantic-core release with no Python 3.13 wheels. 2.8.0 is the first to
# require one that has them (pydantic-core 2.20.0).
# - On Python 3.14, pydantic 1.x before 1.10.25 and 2.x before 2.13 (whose
# pydantic.v1 predates 1.10.25) crash on `import permit` with "unable to
# infer type for attribute". pydantic 2.0-2.11 also have no Python 3.14
# builds.
# uv_build writes these markers to the wheel and sdist as python_full_version
# ranges, and a pre-release sorts below its final release, so a 3.14.0 alpha,
# beta or release candidate matches none of the three lines and gets no
# pydantic requirement. Every final release matches exactly one line.
'pydantic[email]>=1.10.18,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.0,!=2.4.1; python_version < "3.13"',
'pydantic[email]>=1.10.18,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*; python_version == "3.13"',
'pydantic[email]>=1.10.25,!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,!=2.10.*,!=2.11.*,!=2.12.*; python_version >= "3.14"',
# 4.14.0 is the lowest release that works on every supported Python: releases
# before 4.6 break `import permit` on 3.12+, before 4.12 on 3.13+, and 4.12-4.13
# lose TypedDict keys on 3.14.
"typing-extensions>=4.14.0,<5",
]
[project.urls]
Homepage = "https://permit.io"
Documentation = "https://docs.permit.io/sdk/python/quickstart-python"
Repository = "https://github.com/permitio/permit-python"
[dependency-groups]
# Exact pins, so every developer, CI lane and the dev-ceiling audit tree
# resolve the same versions. Dependabot raises them. A pin also gives the CVE
# scan a version to evaluate: a spec with no bound has none, so a package listed
# that way is absent from every audit. These pins are the only place the ruff,
# mypy and typos versions are set: their pre-commit hooks are `repo: local` and
# run the copies `uv run --locked` installs from uv.lock, so CI lints and
# type-checks with exactly these versions, and a Dependabot bump of a pin moves
# the hook with it.
# aioresponses is left out on purpose. No test imports it, and its latest
# release (0.7.9) is incompatible with the aiohttp 3.14.3 floor: every mocked
# request raises "ClientResponse.__init__() missing 1 required keyword-only
# argument: 'stream_writer'". Offline HTTP tests use pytest-httpserver, which
# asserts on real request bodies.
dev = [
# Also what tests/test_typing_surface.py runs on tests/type_check/consumer.py;
# 2.3.1 passes it on Python 3.10-3.14 with either pydantic major.
"mypy==2.3.1",
# Imported directly by the offline tests, which evaluate the version markers
# in [project].dependencies the way an installer does.
"packaging==26.3",
"pre-commit==4.6.2",
# 9.x rather than 8.x: the old 8.3.0 floor is affected by CVE-2025-71176
# (insecure temporary directory handling), fixed in 9.0.3. Caught by this
# repo's own audit gate.
"pytest==9.1.1",
"pytest-asyncio==1.4.0",
"pytest-httpserver==1.1.5",
"ruff==0.16.8",
# The offline tests and the migration skill's tests read [project].dependencies
# from this file, and tomllib is in the standard library only from Python 3.11.
# The marker says == "3.10" rather than < "3.11", which is the same under
# requires-python, because Dependabot skips a requirement whose marker has `<`.
'tomli==2.4.1; python_version == "3.10"',
"typos==1.50.2",
# The uv version CI runs: every setup-uv step reads it from uv.lock
# (version-file), except the publish build job, which pins its own version
# and checksum. Dependabot bumps it like any other pin. The uv-lock pre-commit
# hook runs the uv on PATH, which under `uv run` (as in CI) is this one. Run
# this version locally so uv.lock comes out the same.
"uv==0.12.17",
# Imported directly by the offline tests (Request/Response are used to assert
# on what the SDK actually put on the wire), as well as backing
# pytest-httpserver. Keep it at 3.1.6 or later, the highest fixed version
# across the six advisories that affected the old >=2.3.8 floor
# (CVE-2024-34069, CVE-2024-49766, CVE-2024-49767, CVE-2025-66221,
# CVE-2026-21860, CVE-2026-27199).
"werkzeug==3.1.8",
]
# The SDK supports both pydantic majors (permit/utils/pydantic_version.py), and
# CI runs the suite once per major. Each lane is a group so both resolutions
# live in uv.lock: `uv sync --group pydantic-v1` / `--group pydantic-v2`.
pydantic-v1 = ["pydantic<2"]
pydantic-v2 = ["pydantic>=2"]
[tool.uv]
# The oldest uv that may run here, not the version CI runs: that is the `uv`
# pin in the dev group above, which Dependabot updates. A floor rather than an
# exact pin, because Dependabot runs `uv lock` with its own bundled uv and an
# exact pin fails every one of its updates once that uv differs.
required-version = ">=0.12.17"
# Publish-age cooldown for `uv lock`, matching Dependabot's 7-day cooldown: a
# release is most likely to be a compromised upload in its first days. A security
# fix younger than that is locked with an exclude-newer-package entry here (see
# CONTRIBUTING.md, "Dependencies").
exclude-newer = "7 days"
conflicts = [[{ group = "pydantic-v1" }, { group = "pydantic-v2" }]]
[tool.uv.build-backend]
# Flat layout: the package lives at ./permit, not ./src/permit.
module-root = ""
# The wheel holds the permit package and nothing else, so tests/ and the local
# harness/ tool cannot end up in a consumer's site-packages. The published
# permit==2.8.3 installs a TOP-LEVEL `tests` package there, which shadows the
# consumer's own `tests` module.
# Every file under permit/ goes in, not only .py files. That includes py.typed,
# which tells type checkers to read permit's annotations (PEP 561), and
# _sync_types.pyi, which is how they see the blocking client. CI checks that the
# built wheel and sdist have both.
# The sdist also carries the top-level Markdown files, the migration guide
# among them.
source-include = ["*.md"]
[tool.pytest]
asyncio_mode = "auto"
# The SDK's tests. The migration skill's tests in skills/tests run on their own,
# with skills/tests/pytest.ini (see skills/tests/README.md).
testpaths = ["tests"]
markers = [
'e2e: needs PDP_API_KEY (or another credential), the Permit API and a running PDP. Deselect with -m "not e2e".',
]
# strict_config, strict_markers, strict_xfail and strict_parametrization_ids.
strict = true
# Any warning fails the test that raised it. The one exception is the warning
# `import permit` issues on pydantic 1 on purpose (tests/test_fix_pydantic1_deprecation.py
# checks it in a fresh interpreter).
filterwarnings = [
"error",
"ignore:Support for pydantic 1 is deprecated:DeprecationWarning",
]
[tool.ruff]
line-length = 100
# Generated from the Permit OpenAPI spec by datamodel-code-generator, then
# hand-patched at the top (CONTRIBUTING.md, "Regenerating the API models").
# Linting or formatting it would rewrite ~7k generated lines on every regen
# and bury the real API diff; its content is owned by the generator.
# The migration skill's sample apps are the scanner's test input, written the
# way a permit 2.x project is; their exact text is what the tests assert on.
extend-exclude = ["permit/api/models.py", "skills/tests/fixtures"]
# pre-commit passes file names explicitly, which bypasses exclusions unless
# this is set -- without it the hook would lint and reformat models.py.
force-exclude = true
[tool.ruff.per-file-target-version]
# The migration scanner runs on the project being migrated, before it has moved
# off Python 3.8 or 3.9 (its docstring says so, and CI runs it on 3.9), so no
# fix may use syntax newer than 3.8.
"skills/permit-python-3-migration/scripts/*.py" = "py38"
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
select = ["ALL"]
ignore = [
# Conflict with `ruff format` (listed as such in the ruff formatter docs).
"COM812", # trailing commas are the formatter's call
# Per-file license headers: the Apache-2.0 LICENSE file at the root and
# the package metadata already carry the license.
"CPY001",
# Long messages at the raise site. The alternative is a new exception
# subclass per message, which would widen the public exception API.
"TRY003",
# Module and package docstrings. Users reach the SDK through the `permit`
# package (which has one) and the documented classes and functions; most
# modules hold a single class, so a module docstring would repeat its.
"D100",
"D104",
# Magic-method docstrings restate the protocol (`__repr__`, `__eq__`).
"D105",
# Nested classes are pydantic's `class Config:` blocks: configuration, not API.
"D106",
# Google style documents constructor arguments in the class docstring,
# so a separate `__init__` docstring would repeat it.
"D107",
# The maintainers' limit is on *positional* parameters, enforced by
# PLR0917 (max 5). PLR0913 counts keyword-only parameters too, which is
# the very shape PLR0917 steers towards.
"PLR0913",
# `from module import X as X` is how a module re-exports a name to type
# checkers (PEP 484; mypy's strict mode has no implicit re-export). The SDK
# does this for the blocking classes it declares in permit/_sync_types.pyi.
"PLC0414",
]
[tool.ruff.lint.flake8-annotations]
# `*args: Any` / `**kwargs: Any` are pass-throughs to aiohttp and pydantic,
# which accept arbitrary values; Any elsewhere is still flagged (ANN401).
allow-star-arg-any = true
[tool.ruff.lint.pydocstyle]
# Also resolves the mutually exclusive pairs (D203/D211, D212/D213) and
# turns off the rules Google style contradicts (D401 imperative mood, D413 ...).
convention = "google"
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.flake8-type-checking]
# pydantic evaluates field annotations at runtime, so the imports they use
# must never be moved under `if TYPE_CHECKING:`.
runtime-evaluated-base-classes = ["pydantic.BaseModel", "pydantic.v1.BaseModel"]
[tool.ruff.lint.per-file-ignores]
# Adapted from fastapi.encoders and kept structurally close to it, so upstream
# fixes still port over: its dispatch-by-type function is long by nature, and it
# encodes arbitrary objects, which is what `Any` says.
"permit/api/encoders.py" = ["C901", "PLR0911", "PLR0912", "ANN401"]
# Generated by scripts/generate_sync_stubs.py. Its docstrings are copied from the async
# methods on purpose: they are what an editor shows for the blocking client.
"permit/_sync_types.pyi" = ["PYI021"]
"{tests,skills/tests}/**/*.py" = [
"S101", # assert is how pytest checks things
"S105", # hard-coded fake credentials are test fixtures
"S106", # hard-coded fake credentials are test fixtures
"PLR2004", # literal expected values are the point of an assertion
"SLF001", # white-box tests reach into private state on purpose
"D1", # test names document the test; docstrings where they add something
# End-to-end scenarios run a whole create/check/tear-down story against a live
# backend; splitting them would only scatter one sequence of API side effects.
"C901",
"PLR0912",
"PLR0915",
"PERF203", # try/except in retry and cleanup loops; speed is not what tests measure
"T201", # progress output for long e2e runs; pytest captures it
"BLE001", # e2e tests turn any unexpected exception into a readable pytest.fail
"S603", # subprocesses run the interpreter under test with the test's own arguments
]
# These are standalone CLI programs, not library code: writing the rendered
# report to stdout IS their interface, so the "no print" rule does not apply.
"{.github/scripts,scripts,skills/permit-python-3-migration/scripts}/*.py" = [
"T201",
"INP001", # standalone scripts run by path, not an importable package
]
".github/scripts/test_*.py" = ["S101", "PLR2004", "D1"]
# The migration skill's tests are run by path with their own pytest.ini, like
# the CI scripts' tests, not imported as a package.
"skills/tests/*.py" = ["INP001"]
# The migration scanner runs on Python 3.8, where builtin generics fail at runtime, and
# keeps its annotations evaluated as written rather than add a __future__ import (FA100).
# Each of its checks walks the syntax cases it recognizes in one function; split up, a
# rule would be scattered over helpers that mean nothing on their own. Its literals are
# version components (3.10's minor, the parts `~=` needs) and argument counts (PLR2004).
"skills/permit-python-3-migration/scripts/scan.py" = [
"FA100",
"C901",
"PLR0911",
"PLR0912",
"PLR2004",
]
# A user's code as mypy sees it (tests/test_typing_surface.py): a file mypy is
# pointed at, not a module of the tests package.
"tests/type_check/*.py" = ["INP001"]
[tool.typos.files]
# Generated (see [tool.ruff]); its misspellings come from the OpenAPI spec's
# descriptions and have to be fixed there.
extend-exclude = ["permit/api/models.py"]
[tool.typos.default.extend-words]
# The certifi package, which the migration guide lists among httpx's dependencies.
certifi = "certifi"
[tool.mypy]
python_version = "3.10"
files = ["permit", "tests", ".github/scripts", "scripts", "skills"]
# The sample apps are the migration scanner's test input (see [tool.ruff]).
exclude = ["^skills/tests/fixtures/"]
strict = true
warn_unreachable = true
enable_error_code = [
"deprecated",
"exhaustive-match",
"ignore-without-code",
"mutable-override",
"possibly-undefined",
"redundant-expr",
"redundant-self",
"truthy-bool",
"truthy-iterable",
"unimported-reveal",
"unused-awaitable",
]
# The SDK's models are pydantic-v1 models on both majors: `pydantic.BaseModel`
# under pydantic 1, `pydantic.v1.BaseModel` under pydantic 2. `pydantic.v1.mypy`
# is the v1 plugin and is importable on both, so each CI lane type-checks the
# models the same way. (`pydantic.mypy` under pydantic 2 is the v2 plugin,
# which misreads v1 models.)
plugins = ["pydantic.v1.mypy"]
[tool.pydantic-mypy]
# Model constructors are typed the way pydantic v1 behaves: it coerces input (a
# str for a UUID or EmailStr field) and the API models accept extra fields, so a
# strictly typed or closed `__init__` would reject calls that work. Missing
# required fields are still reported.
init_forbid_extra = false
init_typed = false
warn_required_dynamic_aliases = true
warn_untyped_fields = true
[[tool.mypy.overrides]]
# Generated code (see [tool.ruff] above); checked as a dependency, not linted.
module = ["permit.api.models"]
ignore_errors = true
[[tool.mypy.overrides]]
# Installed only on Python 3.10 (see the dev group), where the standard library has
# no tomllib. mypy reads that branch for python_version 3.10 on any interpreter.
module = ["tomli"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
# These tests declare classes with `metaclass=SyncClass`, which makes their async
# methods blocking at runtime. mypy sees only the `async def` signatures, so every
# call looks like it returns a coroutine.
module = ["tests.test_fix_sync"]
disable_error_code = ["comparison-overlap", "unused-coroutine"]