adapt-server 0.2.2__tar.gz → 0.2.4__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 (112) hide show
  1. {adapt_server-0.2.2 → adapt_server-0.2.4}/PKG-INFO +76 -2
  2. {adapt_server-0.2.2 → adapt_server-0.2.4}/README.md +73 -0
  3. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/__init__.py +1 -1
  4. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/api_keys.py +15 -33
  5. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/locks.py +5 -5
  6. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/ui.py +1 -1
  7. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/users.py +5 -7
  8. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/api_keys.py +51 -1
  9. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/app.py +151 -65
  10. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/audit.py +1 -0
  11. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/dependencies.py +3 -2
  12. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/routes.py +17 -47
  13. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/cli.py +8 -1
  14. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/check.py +16 -0
  15. adapt_server-0.2.4/adapt/commands/reindex.py +44 -0
  16. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/config.py +67 -46
  17. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/discovery.py +1 -15
  18. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/locks.py +1 -0
  19. adapt_server-0.2.4/adapt/mcp.py +162 -0
  20. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/permissions.py +26 -0
  21. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/base.py +25 -0
  22. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/dataset_plugin.py +46 -18
  23. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/html_plugin.py +59 -1
  24. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/markdown_plugin.py +50 -3
  25. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/media_plugin.py +20 -1
  26. adapt_server-0.2.4/adapt/routes.py +127 -0
  27. adapt_server-0.2.4/adapt/routes_search.py +173 -0
  28. adapt_server-0.2.4/adapt/search.py +399 -0
  29. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/security.py +1 -0
  30. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/storage.py +1 -0
  31. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/admin_base.html +7 -0
  32. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/base.html +10 -0
  33. adapt_server-0.2.4/adapt/templates/search_results.html +57 -0
  34. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/utils/query.py +1 -1
  35. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/PKG-INFO +76 -2
  36. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/SOURCES.txt +10 -0
  37. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/requires.txt +2 -1
  38. {adapt_server-0.2.2 → adapt_server-0.2.4}/pyproject.toml +3 -2
  39. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_admin.py +18 -0
  40. adapt_server-0.2.4/tests/test_cache.py +113 -0
  41. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_config.py +2 -0
  42. adapt_server-0.2.4/tests/test_dependencies.py +214 -0
  43. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_integration.py +24 -0
  44. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_markdown_plugin.py +3 -1
  45. adapt_server-0.2.4/tests/test_mcp.py +343 -0
  46. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_readonly.py +6 -8
  47. adapt_server-0.2.4/tests/test_routes.py +28 -0
  48. adapt_server-0.2.4/tests/test_search.py +539 -0
  49. adapt_server-0.2.2/adapt/routes.py +0 -68
  50. {adapt_server-0.2.2 → adapt_server-0.2.4}/LICENSE +0 -0
  51. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/__main__.py +0 -0
  52. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/__init__.py +0 -0
  53. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/audit_logs.py +0 -0
  54. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/cache.py +0 -0
  55. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/groups.py +0 -0
  56. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/models.py +0 -0
  57. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/permissions.py +0 -0
  58. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/admin/resources.py +0 -0
  59. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/__init__.py +0 -0
  60. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/password.py +0 -0
  61. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/auth/session.py +0 -0
  62. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/cache.py +0 -0
  63. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/__init__.py +0 -0
  64. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/addsuperuser.py +0 -0
  65. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/__init__.py +0 -0
  66. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/add_to_group.py +0 -0
  67. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/create_group.py +0 -0
  68. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/create_permissions.py +0 -0
  69. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/create_user.py +0 -0
  70. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/delete_group.py +0 -0
  71. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/delete_user.py +0 -0
  72. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/list_groups.py +0 -0
  73. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/list_resources.py +0 -0
  74. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/list_users.py +0 -0
  75. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/admin/remove_from_group.py +0 -0
  76. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/list_endpoints.py +0 -0
  77. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/commands/serve.py +0 -0
  78. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/models.py +0 -0
  79. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/__init__.py +0 -0
  80. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/csv_plugin.py +0 -0
  81. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/excel_plugin.py +0 -0
  82. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/parquet_plugin.py +0 -0
  83. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/plugins/python_plugin.py +0 -0
  84. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/security_urls.py +0 -0
  85. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/static/admin/app.js +0 -0
  86. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/static/admin/style.css +0 -0
  87. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/admin/index.html +0 -0
  88. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/datatable.html +0 -0
  89. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/landing.html +0 -0
  90. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/login.html +0 -0
  91. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/media_gallery.html +0 -0
  92. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/media_player.html +0 -0
  93. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/templates/profile.html +0 -0
  94. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt/utils/__init__.py +0 -0
  95. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/dependency_links.txt +0 -0
  96. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/entry_points.txt +0 -0
  97. {adapt_server-0.2.2 → adapt_server-0.2.4}/adapt_server.egg-info/top_level.txt +0 -0
  98. {adapt_server-0.2.2 → adapt_server-0.2.4}/setup.cfg +0 -0
  99. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_auth.py +0 -0
  100. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_cli_integration.py +0 -0
  101. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_constraints.py +0 -0
  102. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_csv_plugin.py +0 -0
  103. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_dataset_plugin.py +0 -0
  104. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_discovery.py +0 -0
  105. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_html_plugin.py +0 -0
  106. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_locks.py +0 -0
  107. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_on_delete.py +0 -0
  108. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_parquet_plugin.py +0 -0
  109. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_phase3.py +0 -0
  110. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_plugin_interface.py +0 -0
  111. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_python_plugin.py +0 -0
  112. {adapt_server-0.2.2 → adapt_server-0.2.4}/tests/test_security_urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adapt-server
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Adaptive file-backed FastAPI server that turns datasets into CRUD APIs and UIs.
5
5
  Author-email: notesofcliff <notesofcliff@gmail.com>
6
6
  License-Expression: MIT
@@ -21,7 +21,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
21
21
  Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: fastapi
24
+ Requires-Dist: fastapi>=0.115
25
25
  Requires-Dist: uvicorn
26
26
  Requires-Dist: sqlmodel
27
27
  Requires-Dist: pendulum
@@ -36,6 +36,7 @@ Requires-Dist: pillow
36
36
  Requires-Dist: python-json-logger
37
37
  Requires-Dist: pandas
38
38
  Requires-Dist: fastparquet
39
+ Requires-Dist: mcp<2,>=1.28
39
40
  Provides-Extra: dev
40
41
  Requires-Dist: pytest; extra == "dev"
41
42
  Requires-Dist: httpx; extra == "dev"
@@ -51,6 +52,8 @@ Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
51
52
  - Markdown/HTML become browsable pages
52
53
  - Media files become streaming endpoints and player/gallery UIs
53
54
  - Python files can register custom routers
55
+ - Everything is searchable in one place via full-text `/search`
56
+ - Everything is reachable by agentic tools via an MCP server at `/mcp`
54
57
 
55
58
  ## Quick Start
56
59
 
@@ -81,6 +84,8 @@ Useful URLs:
81
84
  - `/api/<resource>` resource API
82
85
  - `/ui/<resource>` resource UI
83
86
  - `/schema/<resource>` resource schema
87
+ - `/search` full-text search across every resource you can read
88
+ - `/mcp` MCP server for agentic tools (see [MCP Interface](#mcp-interface) below)
84
89
 
85
90
  ## What Adapt Generates
86
91
 
@@ -135,6 +140,73 @@ This reflects the current implementation in the codebase.
135
140
  - Caching with invalidation on mutations
136
141
  - Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
137
142
  - Plugin architecture with companion overrides in `.adapt/`
143
+ - Permission-filtered full-text search across every resource type
144
+ - MCP server for agentic tool access, mounted alongside the REST API
145
+
146
+ ## Full-Text Search
147
+
148
+ `GET /search?q=<query>` searches datasets, Markdown, HTML, and media metadata
149
+ in one ranked list, filtered to what the caller may read — a query term that
150
+ matches a resource you can't see never shows up, and never leaks via the
151
+ result count either.
152
+
153
+ ```bash
154
+ curl -H "X-API-Key: <key>" "http://localhost:8000/search?q=parental+leave"
155
+ ```
156
+
157
+ The index refreshes incrementally on startup (`search_on_startup`, default
158
+ `true`) and can be rebuilt on demand with `adapt reindex <root>`. See the
159
+ [API Reference](docs/manual/api_reference.md#search-endpoint) for query
160
+ parameters and result shape.
161
+
162
+ ## MCP Interface
163
+
164
+ Adapt mounts a [Model Context Protocol](https://modelcontextprotocol.io)
165
+ server at `/mcp`, on the same host/port as everything else, exposing five
166
+ tools that wrap the same permission checks and plugin methods as the REST
167
+ API — `list_resources`, `get_schema`, `read_resource`, `write_resource`, and
168
+ `search`. There's no separate process, no separate API surface, and no
169
+ extra permission model to maintain.
170
+
171
+ Minimal walkthrough — create an account for the agent, grant it read access,
172
+ mint an API key, and connect a client:
173
+
174
+ ```bash
175
+ adapt addsuperuser /path/to/docroot --username admin
176
+ adapt serve /path/to/docroot &
177
+
178
+ adapt admin create-permissions /path/to/docroot __all__
179
+ adapt admin create-user /path/to/docroot --username agent --password <strong-password>
180
+ adapt admin add-to-group /path/to/docroot --username agent --group <resource>_readonly
181
+ ```
182
+
183
+ Log in as `agent` and self-issue an API key from `/profile` (any
184
+ authenticated user can create their own key — no superuser needed), then
185
+ point a client at `/mcp` with that key:
186
+
187
+ ```bash
188
+ # Claude Code CLI
189
+ claude mcp add --transport http adapt http://localhost:8000/mcp \
190
+ --header "X-API-Key: <key>"
191
+ ```
192
+
193
+ ```json
194
+ // Generic MCP client config (Claude Desktop and similar)
195
+ {
196
+ "mcpServers": {
197
+ "adapt": {
198
+ "url": "http://localhost:8000/mcp",
199
+ "headers": { "X-API-Key": "<key>" }
200
+ }
201
+ }
202
+ }
203
+ ```
204
+
205
+ MCP requires an API key on every call — there's no session-cookie or
206
+ anonymous path, since MCP has no concept of a browser session. Set
207
+ `mcp_enabled: false` in `.adapt/conf.json` (or `ADAPT_MCP_ENABLED=false`) to
208
+ remove `/mcp` entirely. Full walkthrough, troubleshooting, and the tool
209
+ reference table: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md).
138
210
 
139
211
  ## Dataset Mutation Envelope
140
212
 
@@ -156,6 +228,7 @@ adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload
156
228
  adapt check <root>
157
229
  adapt addsuperuser <root> --username <name>
158
230
  adapt list-endpoints <root>
231
+ adapt reindex <root> [--force]
159
232
  adapt admin list-resources <root>
160
233
  adapt admin create-permissions <root> __all__
161
234
  ```
@@ -169,6 +242,7 @@ Detailed docs live under `docs/manual/`.
169
242
  - Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
170
243
  - Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
171
244
  - API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
245
+ - MCP guide: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md)
172
246
  - Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
173
247
 
174
248
  ## License
@@ -6,6 +6,8 @@ Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
6
6
  - Markdown/HTML become browsable pages
7
7
  - Media files become streaming endpoints and player/gallery UIs
8
8
  - Python files can register custom routers
9
+ - Everything is searchable in one place via full-text `/search`
10
+ - Everything is reachable by agentic tools via an MCP server at `/mcp`
9
11
 
10
12
  ## Quick Start
11
13
 
@@ -36,6 +38,8 @@ Useful URLs:
36
38
  - `/api/<resource>` resource API
37
39
  - `/ui/<resource>` resource UI
38
40
  - `/schema/<resource>` resource schema
41
+ - `/search` full-text search across every resource you can read
42
+ - `/mcp` MCP server for agentic tools (see [MCP Interface](#mcp-interface) below)
39
43
 
40
44
  ## What Adapt Generates
41
45
 
@@ -90,6 +94,73 @@ This reflects the current implementation in the codebase.
90
94
  - Caching with invalidation on mutations
91
95
  - Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
92
96
  - Plugin architecture with companion overrides in `.adapt/`
97
+ - Permission-filtered full-text search across every resource type
98
+ - MCP server for agentic tool access, mounted alongside the REST API
99
+
100
+ ## Full-Text Search
101
+
102
+ `GET /search?q=<query>` searches datasets, Markdown, HTML, and media metadata
103
+ in one ranked list, filtered to what the caller may read — a query term that
104
+ matches a resource you can't see never shows up, and never leaks via the
105
+ result count either.
106
+
107
+ ```bash
108
+ curl -H "X-API-Key: <key>" "http://localhost:8000/search?q=parental+leave"
109
+ ```
110
+
111
+ The index refreshes incrementally on startup (`search_on_startup`, default
112
+ `true`) and can be rebuilt on demand with `adapt reindex <root>`. See the
113
+ [API Reference](docs/manual/api_reference.md#search-endpoint) for query
114
+ parameters and result shape.
115
+
116
+ ## MCP Interface
117
+
118
+ Adapt mounts a [Model Context Protocol](https://modelcontextprotocol.io)
119
+ server at `/mcp`, on the same host/port as everything else, exposing five
120
+ tools that wrap the same permission checks and plugin methods as the REST
121
+ API — `list_resources`, `get_schema`, `read_resource`, `write_resource`, and
122
+ `search`. There's no separate process, no separate API surface, and no
123
+ extra permission model to maintain.
124
+
125
+ Minimal walkthrough — create an account for the agent, grant it read access,
126
+ mint an API key, and connect a client:
127
+
128
+ ```bash
129
+ adapt addsuperuser /path/to/docroot --username admin
130
+ adapt serve /path/to/docroot &
131
+
132
+ adapt admin create-permissions /path/to/docroot __all__
133
+ adapt admin create-user /path/to/docroot --username agent --password <strong-password>
134
+ adapt admin add-to-group /path/to/docroot --username agent --group <resource>_readonly
135
+ ```
136
+
137
+ Log in as `agent` and self-issue an API key from `/profile` (any
138
+ authenticated user can create their own key — no superuser needed), then
139
+ point a client at `/mcp` with that key:
140
+
141
+ ```bash
142
+ # Claude Code CLI
143
+ claude mcp add --transport http adapt http://localhost:8000/mcp \
144
+ --header "X-API-Key: <key>"
145
+ ```
146
+
147
+ ```json
148
+ // Generic MCP client config (Claude Desktop and similar)
149
+ {
150
+ "mcpServers": {
151
+ "adapt": {
152
+ "url": "http://localhost:8000/mcp",
153
+ "headers": { "X-API-Key": "<key>" }
154
+ }
155
+ }
156
+ }
157
+ ```
158
+
159
+ MCP requires an API key on every call — there's no session-cookie or
160
+ anonymous path, since MCP has no concept of a browser session. Set
161
+ `mcp_enabled: false` in `.adapt/conf.json` (or `ADAPT_MCP_ENABLED=false`) to
162
+ remove `/mcp` entirely. Full walkthrough, troubleshooting, and the tool
163
+ reference table: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md).
93
164
 
94
165
  ## Dataset Mutation Envelope
95
166
 
@@ -111,6 +182,7 @@ adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload
111
182
  adapt check <root>
112
183
  adapt addsuperuser <root> --username <name>
113
184
  adapt list-endpoints <root>
185
+ adapt reindex <root> [--force]
114
186
  adapt admin list-resources <root>
115
187
  adapt admin create-permissions <root> __all__
116
188
  ```
@@ -124,6 +196,7 @@ Detailed docs live under `docs/manual/`.
124
196
  - Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
125
197
  - Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
126
198
  - API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
199
+ - MCP guide: [docs/manual/mcp_guide.md](docs/manual/mcp_guide.md)
127
200
  - Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
128
201
 
129
202
  ## License
@@ -1,3 +1,3 @@
1
1
  """Adapt package namespace."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -1,13 +1,14 @@
1
+ import json
2
+ import logging
3
+ from typing import List
4
+
1
5
  from fastapi import Depends, HTTPException, Request, Query
2
6
  from sqlalchemy import asc, desc
3
7
  from sqlmodel import Session, select
4
- from typing import List
5
- from datetime import datetime, timedelta, timezone
6
- import logging
7
8
 
8
9
  from ..auth import require_superuser
9
10
  from ..storage import User, APIKey, get_db_session
10
- from ..api_keys import generate_api_key
11
+ from ..api_keys import create_api_key_record, revoke_api_key_record
11
12
  from ..audit import log_action
12
13
  from . import router
13
14
  from .models import APIKeyCreate
@@ -27,8 +28,6 @@ def list_api_keys(
27
28
  user = Depends(require_superuser)
28
29
  ):
29
30
  """List all API keys in the system with optional query parameters."""
30
- import json
31
-
32
31
  stmt = select(APIKey)
33
32
 
34
33
  if user_id is not None:
@@ -73,28 +72,15 @@ def create_api_key(key_data: APIKeyCreate, request: Request, db: Session = Depen
73
72
  if not target_user:
74
73
  logger.warning("Attempted to create API key for non-existent user %d", key_data.user_id)
75
74
  raise HTTPException(status_code=404, detail="User not found")
76
-
77
- raw_key, key_hash = generate_api_key()
78
-
79
- expires_at = None
80
- if key_data.expires_in_days is not None:
81
- if key_data.expires_in_days > 365:
82
- raise HTTPException(status_code=400, detail="Expiration cannot exceed 1 year (365 days)")
83
- expires_at = datetime.now(tz=timezone.utc) + timedelta(days=key_data.expires_in_days)
84
-
85
- api_key = APIKey(
86
- user_id=key_data.user_id,
87
- key_hash=key_hash,
88
- description=key_data.description,
89
- expires_at=expires_at
90
- )
91
- db.add(api_key)
92
- db.commit()
93
- db.refresh(api_key)
94
-
75
+
76
+ try:
77
+ raw_key, api_key = create_api_key_record(db, key_data.user_id, key_data.description, key_data.expires_in_days)
78
+ except ValueError as e:
79
+ raise HTTPException(status_code=400, detail=str(e))
80
+
95
81
  log_action(request, "create_api_key", "apikey", f"Created API key for user {target_user.username}", user.id)
96
82
  logger.info("Created API key %d for user %s", api_key.id, target_user.username)
97
-
83
+
98
84
  # Return the raw key only once!
99
85
  return {
100
86
  "id": api_key.id,
@@ -108,16 +94,12 @@ def create_api_key(key_data: APIKeyCreate, request: Request, db: Session = Depen
108
94
  @router.delete("/api-keys/{key_id}")
109
95
  def revoke_api_key(key_id: int, request: Request, db: Session = Depends(get_db_session), user = Depends(require_superuser)):
110
96
  """Revoke an API key by ID."""
111
- api_key = db.get(APIKey, key_id)
97
+ api_key = revoke_api_key_record(db, key_id)
112
98
  if not api_key:
113
99
  logger.warning("Attempted to revoke non-existent API key %d", key_id)
114
100
  raise HTTPException(status_code=404, detail="API Key not found")
115
-
116
- api_key.is_active = False
117
- db.add(api_key)
118
- db.commit()
119
-
101
+
120
102
  log_action(request, "revoke_api_key", "apikey", f"Revoked API key {key_id}", user.id)
121
103
  logger.info("Revoked API key %d", key_id)
122
-
104
+
123
105
  return {"success": True}
@@ -1,11 +1,14 @@
1
+ import json
2
+ import logging
3
+ from typing import List
4
+
1
5
  from fastapi import Depends, HTTPException, Request, Query
2
6
  from sqlmodel import Session, select
3
- from typing import List
4
- import logging
5
7
 
6
8
  from ..auth import require_superuser
7
9
  from ..storage import LockRecord, get_db_session
8
10
  from ..locks import LockManager
11
+ from ..utils.query import apply_filter, apply_sort, apply_pagination
9
12
  from ..audit import log_action
10
13
  from . import router
11
14
 
@@ -22,9 +25,6 @@ def list_locks(
22
25
  user = Depends(require_superuser)
23
26
  ):
24
27
  """List all active locks with optional query parameters."""
25
- from ..utils.query import apply_filter, apply_sort, apply_pagination
26
- import json
27
-
28
28
  locks = db.exec(select(LockRecord)).all()
29
29
 
30
30
  # Convert to dicts for filtering/sorting
@@ -30,4 +30,4 @@ def admin_ui(request: Request):
30
30
 
31
31
  # Serve the admin template
32
32
  logger.debug("Rendering admin UI for superuser %s", user.username)
33
- return request.app.state.templates.TemplateResponse("admin/index.html", context)
33
+ return request.app.state.templates.TemplateResponse(request, "admin/index.html", context)
@@ -1,10 +1,13 @@
1
+ import json
2
+ import logging
3
+ from typing import List
4
+
1
5
  from fastapi import Depends, HTTPException, Request, Query
2
6
  from sqlmodel import Session, select
3
- from typing import List
4
- import logging
5
7
 
6
8
  from ..auth import require_superuser, hash_password
7
9
  from ..storage import User, get_db_session
10
+ from ..utils.query import apply_filter, apply_sort, apply_pagination
8
11
  from ..audit import log_action
9
12
  from . import router
10
13
  from .models import UserCreate, UserPublic
@@ -22,9 +25,6 @@ def list_users(
22
25
  user: User = Depends(require_superuser)
23
26
  ):
24
27
  """List all users with optional query parameters."""
25
- from ..utils.query import apply_filter, apply_sort, apply_pagination
26
- import json
27
-
28
28
  query = select(User)
29
29
  users = db.exec(query).all()
30
30
 
@@ -75,7 +75,6 @@ def create_user(user_data: UserCreate, request: Request, db: Session = Depends(g
75
75
  """Create a new user."""
76
76
  # Check if server is in read-only mode
77
77
  if request.app.state.config.readonly:
78
- from fastapi import HTTPException
79
78
  raise HTTPException(status_code=405, detail="Server is in read-only mode")
80
79
 
81
80
  existing = db.exec(select(User).where(User.username == user_data.username)).first()
@@ -109,7 +108,6 @@ def delete_user(user_id: int, request: Request, db: Session = Depends(get_db_ses
109
108
  """Delete a user by ID."""
110
109
  # Check if server is in read-only mode
111
110
  if request.app.state.config.readonly:
112
- from fastapi import HTTPException
113
111
  raise HTTPException(status_code=405, detail="Server is in read-only mode")
114
112
 
115
113
  target = db.get(User, user_id)
@@ -1,9 +1,10 @@
1
+ """adapt.api_keys — API key generation, verification, and lifecycle helpers."""
1
2
  from __future__ import annotations
2
3
 
3
4
  import logging
4
5
  import secrets
5
6
  import hashlib
6
- from datetime import datetime, timezone
7
+ from datetime import datetime, timedelta, timezone
7
8
  from sqlmodel import Session, select
8
9
  from fastapi import Request, HTTPException, Security
9
10
  from fastapi.security import APIKeyHeader
@@ -60,6 +61,55 @@ def verify_api_key(db: Session, raw_key: str) -> User | None:
60
61
  logger.info(f"API key verified successfully for user {api_key.user_id}")
61
62
  return db.get(User, api_key.user_id)
62
63
 
64
+ def create_api_key_record(
65
+ db: Session,
66
+ user_id: int,
67
+ description: str | None,
68
+ expires_in_days: int | None,
69
+ ) -> tuple[str, APIKey]:
70
+ """Create and persist an API key. Returns (raw_key, api_key).
71
+
72
+ Raises ValueError if expires_in_days exceeds 365.
73
+ """
74
+ if expires_in_days is not None and expires_in_days > 365:
75
+ raise ValueError("Expiration cannot exceed 1 year (365 days)")
76
+ expires_at = None
77
+ if expires_in_days is not None:
78
+ expires_at = datetime.now(tz=timezone.utc) + timedelta(days=expires_in_days)
79
+ raw_key, key_hash = generate_api_key()
80
+ api_key = APIKey(
81
+ user_id=user_id,
82
+ key_hash=key_hash,
83
+ description=description,
84
+ expires_at=expires_at,
85
+ created_at=datetime.now(tz=timezone.utc),
86
+ is_active=True,
87
+ )
88
+ db.add(api_key)
89
+ db.commit()
90
+ db.refresh(api_key)
91
+ return raw_key, api_key
92
+
93
+
94
+ def revoke_api_key_record(
95
+ db: Session,
96
+ key_id: int,
97
+ *,
98
+ owner_id: int | None = None,
99
+ ) -> APIKey | None:
100
+ """Set an API key inactive. If owner_id is given, enforces ownership.
101
+
102
+ Returns the key on success, None if not found or ownership mismatch.
103
+ """
104
+ api_key = db.get(APIKey, key_id)
105
+ if not api_key or (owner_id is not None and api_key.user_id != owner_id):
106
+ return None
107
+ api_key.is_active = False
108
+ db.add(api_key)
109
+ db.commit()
110
+ return api_key
111
+
112
+
63
113
  def get_user_from_api_key(
64
114
  request: Request,
65
115
  api_key: str = Security(API_KEY_HEADER)