ccp4i2-api 0.3.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,12 @@
1
+ # TypeScript build output (tsc → lib/)
2
+ lib/
3
+ # Python build output (python -m build → dist/)
4
+ dist/
5
+ build/
6
+ node_modules/
7
+ *.egg-info/
8
+ __pycache__/
9
+ *.pyc
10
+ .pytest_cache/
11
+ coverage/
12
+ .coverage
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: ccp4i2-api
3
+ Version: 0.3.0
4
+ Summary: Shared API contract (auth, api-fetch, request/response types) for CCP4i2 and consumers
5
+ Project-URL: Homepage, https://github.com/ccp4/ccp4i2/tree/django-sliced/packages/ccp4i2-api
6
+ Project-URL: Repository, https://github.com/ccp4/ccp4i2
7
+ Project-URL: Issues, https://github.com/ccp4/ccp4i2/issues
8
+ Author: CCP4i2 contributors
9
+ License: LGPL-3.0-or-later
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: certifi>=2024.0.0
12
+ Requires-Dist: django<5.0,>=4.2
13
+ Requires-Dist: djangorestframework>=3.14
14
+ Requires-Dist: pyjwt[crypto]>=2.10
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest; extra == 'test'
17
+ Requires-Dist: pytest-django; extra == 'test'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # `@ccp4/ccp4i2-api` / `ccp4i2-api`
21
+
22
+ Shared API contract for CCP4i2 and consumers (CCP4i2 Compounds, third-party
23
+ integrators) — auth handshake, api-fetch helpers, and request/response
24
+ types. One package, two language artifacts: TypeScript on the client side
25
+ (browser, Electron) and Python on the server side (Django middleware, DRF
26
+ authentication). Both halves agree on the canonical bearer-token format,
27
+ 401 response shape, and the typed payloads carried over the authenticated
28
+ channel.
29
+
30
+ ## Status
31
+
32
+ **Draft v0 — published to npm + PyPI from the in-tree workspace.** Source
33
+ of truth lives at
34
+ [`packages/ccp4i2-api/`](https://github.com/ccp4/ccp4i2/tree/django-sliced/packages/ccp4i2-api)
35
+ inside the `ccp4/ccp4i2` monorepo on the `django-sliced` branch and stays
36
+ there. The companion `ccp4/ccp4i2-api` GitHub repo (created for the npm
37
+ namespace and external visibility) does not host source.
38
+
39
+ Versions `0.1.0`–`0.3.0` were published under the previous name
40
+ `@ccp4/ccp4i2-auth` / `ccp4i2-auth`; the package was renamed at `0.3.0`
41
+ because its scope had grown beyond auth to cover the broader API contract.
42
+ The old name is unpublished/yanked; consumers should depend on
43
+ `@ccp4/ccp4i2-api` and `ccp4i2-api` from `0.3.0` onward.
44
+
45
+ Versioning follows semver from `0.x.y` onwards. The v0 contract is
46
+ documented in
47
+ [`docs/CCP4I2_SERVICE_CONTRACT.md`](https://github.com/ccp4/ccp4i2/blob/django-sliced/docs/CCP4I2_SERVICE_CONTRACT.md);
48
+ field stability promises take effect from this version.
49
+
50
+ ## Layout
51
+
52
+ | Path | Purpose |
53
+ |---|---|
54
+ | `src/` | TypeScript source. Built to `lib/` via `npm run build`. |
55
+ | `lib/` | Built TypeScript output. Generated; gitignored. |
56
+ | `dist/` | Python distribution output (`python -m build`). Generated; gitignored. Kept distinct from `lib/` so `twine upload dist/*` doesn't accidentally pick up TypeScript artefacts. |
57
+ | `ccp4i2_api/` | Python source. Installed editable via `pip install -e .`. |
58
+ | `tests/js/` | TypeScript tests (vitest, when added). |
59
+ | `tests/python/` | Python tests (pytest, when added). |
60
+
61
+ ## Consumer wiring
62
+
63
+ In-monorepo consumers can reference this package by local path for fast
64
+ iteration; out-of-monorepo consumers pull the published versions.
65
+
66
+ **TypeScript** — in-monorepo (`client/package.json`):
67
+
68
+ ```json
69
+ "dependencies": {
70
+ "@ccp4/ccp4i2-api": "file:../packages/ccp4i2-api"
71
+ }
72
+ ```
73
+
74
+ (Path depth varies by consumer location.) Out-of-monorepo consumers use the
75
+ published range, e.g. `"@ccp4/ccp4i2-api": "^0.3.0"`.
76
+
77
+ **Python** — in-monorepo (`Docker/server/Dockerfile`, local dev setup):
78
+
79
+ ```bash
80
+ pip install -e packages/ccp4i2-api/
81
+ ```
82
+
83
+ Out-of-monorepo consumers `pip install ccp4i2-api>=0.3`.
84
+
85
+ ## Development
86
+
87
+ ```bash
88
+ # TypeScript
89
+ cd packages/ccp4i2-api
90
+ npm install
91
+ npm run build # produces lib/
92
+ npm run watch # rebuilds on change
93
+
94
+ # Python
95
+ cd packages/ccp4i2-api
96
+ ccp4-python -m pip install -e .
97
+ ccp4-python -c "import ccp4i2_api; print(ccp4i2_api.__version__)"
98
+ ```
@@ -0,0 +1,79 @@
1
+ # `@ccp4/ccp4i2-api` / `ccp4i2-api`
2
+
3
+ Shared API contract for CCP4i2 and consumers (CCP4i2 Compounds, third-party
4
+ integrators) — auth handshake, api-fetch helpers, and request/response
5
+ types. One package, two language artifacts: TypeScript on the client side
6
+ (browser, Electron) and Python on the server side (Django middleware, DRF
7
+ authentication). Both halves agree on the canonical bearer-token format,
8
+ 401 response shape, and the typed payloads carried over the authenticated
9
+ channel.
10
+
11
+ ## Status
12
+
13
+ **Draft v0 — published to npm + PyPI from the in-tree workspace.** Source
14
+ of truth lives at
15
+ [`packages/ccp4i2-api/`](https://github.com/ccp4/ccp4i2/tree/django-sliced/packages/ccp4i2-api)
16
+ inside the `ccp4/ccp4i2` monorepo on the `django-sliced` branch and stays
17
+ there. The companion `ccp4/ccp4i2-api` GitHub repo (created for the npm
18
+ namespace and external visibility) does not host source.
19
+
20
+ Versions `0.1.0`–`0.3.0` were published under the previous name
21
+ `@ccp4/ccp4i2-auth` / `ccp4i2-auth`; the package was renamed at `0.3.0`
22
+ because its scope had grown beyond auth to cover the broader API contract.
23
+ The old name is unpublished/yanked; consumers should depend on
24
+ `@ccp4/ccp4i2-api` and `ccp4i2-api` from `0.3.0` onward.
25
+
26
+ Versioning follows semver from `0.x.y` onwards. The v0 contract is
27
+ documented in
28
+ [`docs/CCP4I2_SERVICE_CONTRACT.md`](https://github.com/ccp4/ccp4i2/blob/django-sliced/docs/CCP4I2_SERVICE_CONTRACT.md);
29
+ field stability promises take effect from this version.
30
+
31
+ ## Layout
32
+
33
+ | Path | Purpose |
34
+ |---|---|
35
+ | `src/` | TypeScript source. Built to `lib/` via `npm run build`. |
36
+ | `lib/` | Built TypeScript output. Generated; gitignored. |
37
+ | `dist/` | Python distribution output (`python -m build`). Generated; gitignored. Kept distinct from `lib/` so `twine upload dist/*` doesn't accidentally pick up TypeScript artefacts. |
38
+ | `ccp4i2_api/` | Python source. Installed editable via `pip install -e .`. |
39
+ | `tests/js/` | TypeScript tests (vitest, when added). |
40
+ | `tests/python/` | Python tests (pytest, when added). |
41
+
42
+ ## Consumer wiring
43
+
44
+ In-monorepo consumers can reference this package by local path for fast
45
+ iteration; out-of-monorepo consumers pull the published versions.
46
+
47
+ **TypeScript** — in-monorepo (`client/package.json`):
48
+
49
+ ```json
50
+ "dependencies": {
51
+ "@ccp4/ccp4i2-api": "file:../packages/ccp4i2-api"
52
+ }
53
+ ```
54
+
55
+ (Path depth varies by consumer location.) Out-of-monorepo consumers use the
56
+ published range, e.g. `"@ccp4/ccp4i2-api": "^0.3.0"`.
57
+
58
+ **Python** — in-monorepo (`Docker/server/Dockerfile`, local dev setup):
59
+
60
+ ```bash
61
+ pip install -e packages/ccp4i2-api/
62
+ ```
63
+
64
+ Out-of-monorepo consumers `pip install ccp4i2-api>=0.3`.
65
+
66
+ ## Development
67
+
68
+ ```bash
69
+ # TypeScript
70
+ cd packages/ccp4i2-api
71
+ npm install
72
+ npm run build # produces lib/
73
+ npm run watch # rebuilds on change
74
+
75
+ # Python
76
+ cd packages/ccp4i2-api
77
+ ccp4-python -m pip install -e .
78
+ ccp4-python -c "import ccp4i2_api; print(ccp4i2_api.__version__)"
79
+ ```
@@ -0,0 +1,13 @@
1
+ """Shared API contract for CCP4i2 and consumers.
2
+
3
+ This package is the Python half of a bilingual API library; the TypeScript
4
+ half is published as ``@ccp4/ccp4i2-api``. Both halves implement the same
5
+ canonical bearer-token format, 401 response shape, and typed payloads
6
+ carried over the authenticated channel.
7
+
8
+ See the package README for current status and
9
+ ``docs/CCP4I2_SERVICE_CONTRACT.md`` in the ccp4i2 monorepo for the
10
+ contract specification.
11
+ """
12
+
13
+ __version__ = "0.3.0"
@@ -0,0 +1,58 @@
1
+ """DRF authentication classes for CCP4i2.
2
+
3
+ These classes work in tandem with the middleware in
4
+ ``ccp4i2_api.middleware`` — the middleware validates the bearer token
5
+ and sets ``request.user`` plus the ``REQUEST_FLAG_ATTR`` trust flag; the
6
+ DRF auth class checks the flag and surfaces ``request.user`` to DRF's
7
+ ``IsAuthenticated`` permission.
8
+ """
9
+
10
+ from rest_framework.authentication import BaseAuthentication
11
+
12
+ from .middleware.base import REQUEST_FLAG_ATTR
13
+
14
+
15
+ class AzureADAuthentication(BaseAuthentication):
16
+ """
17
+ DRF authentication class that uses the user set by AzureADAuthMiddleware.
18
+
19
+ This allows DRF's IsAuthenticated permission to work with our middleware.
20
+ The middleware does the actual JWT validation; this class just passes
21
+ the authenticated user to DRF.
22
+
23
+ In dev/Electron mode (CCP4I2_REQUIRE_AUTH not set), the middleware
24
+ auto-assigns a dev_admin user, which this class also recognizes.
25
+
26
+ Security: Only trusts users when our middleware has explicitly processed
27
+ the request (marked by ``REQUEST_FLAG_ATTR`` attribute). This prevents
28
+ spoofing attacks where a request might have ``request.user`` set by
29
+ some other means.
30
+
31
+ Note: this class is bound to the trust flag, not to the AzureAD chain
32
+ specifically; it works equally for any middleware that inherits from
33
+ ``BaseAuthMiddleware`` (e.g., LocalSessionAuthMiddleware in desktop
34
+ mode), because they all set the same flag.
35
+ """
36
+
37
+ def authenticate(self, request):
38
+ """
39
+ Return the user if already authenticated by middleware, None otherwise.
40
+
41
+ Returns:
42
+ Tuple of (user, None) if authenticated, None if not.
43
+ """
44
+ # Check if middleware already validated and set user
45
+ # The middleware sets these attributes on the underlying Django request
46
+ django_request = getattr(request, '_request', request)
47
+
48
+ # Security check: only trust users set by our middleware
49
+ # This prevents spoofing where request.user might be set elsewhere
50
+ if not getattr(django_request, REQUEST_FLAG_ATTR, False):
51
+ return None
52
+
53
+ # Middleware ran - trust the user it set
54
+ user = getattr(django_request, 'user', None)
55
+ if user and user.is_authenticated and not user.is_anonymous:
56
+ return (user, None)
57
+
58
+ return None
@@ -0,0 +1,18 @@
1
+ """Exceptions used by the shared auth middleware contract."""
2
+
3
+
4
+ class AuthenticationFailed(Exception):
5
+ """Raised by ``BaseAuthMiddleware.authenticate()`` to signal a 401.
6
+
7
+ The exception's first argument is the message returned in the canonical
8
+ 401 response body (``{"success": false, "error": "..."}``).
9
+ """
10
+
11
+
12
+ class AuthorizationFailed(Exception):
13
+ """Raised by ``BaseAuthMiddleware.authenticate()`` to signal a 403.
14
+
15
+ Use when the request is *authenticated* (we know who is calling) but
16
+ *not authorized* (e.g., not a member of an allowed group). The first
17
+ argument is the message returned in the canonical 403 response body.
18
+ """
@@ -0,0 +1,23 @@
1
+ """Django middleware shipped by the shared auth package.
2
+
3
+ Each module in this package implements one auth scheme; consumers select
4
+ which to enable via Django ``MIDDLEWARE`` settings. All non-dev schemes
5
+ inherit from ``BaseAuthMiddleware`` (or will, after the AzureAD refactor),
6
+ which defines the canonical 401 response shape and the ``REQUEST_FLAG_ATTR``
7
+ trust signal.
8
+ """
9
+
10
+ from .azure_ad import AzureADAuthMiddleware
11
+ from .base import REQUEST_FLAG_ATTR, BaseAuthMiddleware
12
+ from .dev import DevAuthMiddleware
13
+ from .dev_admin import DevAdminMiddleware
14
+ from .local_session import LocalSessionAuthMiddleware
15
+
16
+ __all__ = [
17
+ "AzureADAuthMiddleware",
18
+ "BaseAuthMiddleware",
19
+ "DevAdminMiddleware",
20
+ "DevAuthMiddleware",
21
+ "LocalSessionAuthMiddleware",
22
+ "REQUEST_FLAG_ATTR",
23
+ ]