test-data-service 0.2.0__tar.gz → 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.
Files changed (37) hide show
  1. {test_data_service-0.2.0/src/test_data_service.egg-info → test_data_service-0.3.0}/PKG-INFO +78 -49
  2. {test_data_service-0.2.0 → test_data_service-0.3.0}/pyproject.toml +2 -2
  3. {test_data_service-0.2.0 → test_data_service-0.3.0}/readme.md +76 -47
  4. {test_data_service-0.2.0 → test_data_service-0.3.0/src/test_data_service.egg-info}/PKG-INFO +78 -49
  5. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/test_data_service.egg-info/SOURCES.txt +1 -0
  6. test_data_service-0.3.0/src/testdataservice/__init__.py +36 -0
  7. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/app.py +187 -90
  8. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/config.py +12 -0
  9. test_data_service-0.3.0/src/testdataservice/sqlite_storage.py +215 -0
  10. test_data_service-0.3.0/src/testdataservice/storage.py +336 -0
  11. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/web/static/app.js +249 -107
  12. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/web/static/style.css +46 -0
  13. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/web/templates/index.html +47 -21
  14. test_data_service-0.3.0/tds.example.yml +8 -0
  15. test_data_service-0.3.0/tests/test_service.py +393 -0
  16. test_data_service-0.2.0/src/testdataservice/__init__.py +0 -26
  17. test_data_service-0.2.0/src/testdataservice/storage.py +0 -219
  18. test_data_service-0.2.0/tds.example.yml +0 -6
  19. test_data_service-0.2.0/tests/test_service.py +0 -449
  20. {test_data_service-0.2.0 → test_data_service-0.3.0}/LICENSE +0 -0
  21. {test_data_service-0.2.0 → test_data_service-0.3.0}/MANIFEST.in +0 -0
  22. {test_data_service-0.2.0 → test_data_service-0.3.0}/requirements-dev.txt +0 -0
  23. {test_data_service-0.2.0 → test_data_service-0.3.0}/requirements.txt +0 -0
  24. {test_data_service-0.2.0 → test_data_service-0.3.0}/setup.cfg +0 -0
  25. {test_data_service-0.2.0 → test_data_service-0.3.0}/setup.py +0 -0
  26. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/test_data_service.egg-info/dependency_links.txt +0 -0
  27. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/test_data_service.egg-info/entry_points.txt +0 -0
  28. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/test_data_service.egg-info/requires.txt +0 -0
  29. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/test_data_service.egg-info/top_level.txt +0 -0
  30. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/__main__.py +0 -0
  31. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/accounts.py +0 -0
  32. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/auth.py +0 -0
  33. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/cli.py +0 -0
  34. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/keystore.py +0 -0
  35. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/passwords.py +0 -0
  36. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/sessions.py +0 -0
  37. {test_data_service-0.2.0 → test_data_service-0.3.0}/src/testdataservice/types.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-data-service
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A disk-backed test data service with a REST API and web interface
5
- Author-email: Roy de Kleijn <dekleijn.roy@gmail.com>
5
+ Author-email: Roy de Kleijn <info@testsmith.io>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/testsmith-io/test-data-service
8
8
  Project-URL: Documentation, https://github.com/testsmith-io/test-data-service#readme
@@ -81,46 +81,68 @@ port: 8000
81
81
 
82
82
  ### Data model
83
83
 
84
- Each entry is a `key` / `value` pair inside a **namespace**. A value is one
85
- of four types: `string`, `boolean`, `number`, or `array`. The type is stored
86
- alongside the value and validated on write.
84
+ Three levels: **namespace → table → row**.
85
+
86
+ * A **namespace** is the unit of access control.
87
+ * A namespace holds any number of **tables**.
88
+ * A table holds **rows**, and each row is a record with an **arbitrary set of
89
+ fields**. Field values can be any JSON (string, number, boolean, array,
90
+ object, null).
91
+ * A table can also have **designed columns** — a persisted column list you set
92
+ up front (the "schema"), which shows even before any rows exist. The
93
+ *effective* columns of a table are its designed columns plus any extra fields
94
+ that appear in its rows, so the layout stays dynamic.
87
95
 
88
96
  ### REST API
89
97
 
90
- `POST` targets the namespace (collection) with the key **in the body**; `PUT`
91
- targets a specific entry URI. They are not interchangeable — `POST` only creates,
92
- `PUT` only updates.
93
-
94
- | Method | Path | Permission | Description |
95
- |----------|-------------------------|------------|--------------------------------------|
96
- | `GET` | `/api/health` | — | Health + auth status |
97
- | `GET` | `/api/whoami` | — | Caller's access summary |
98
- | `GET` | `/api/namespaces` | any key | List accessible namespaces |
99
- | `GET` | `/api/{ns}` | read `ns` | List entries in a namespace |
100
- | `POST` | `/api/{ns}` | write `ns` | Create an entry (`409` if key exists) |
101
- | `GET` | `/api/{ns}/{key}` | read `ns` | Get one entry |
102
- | `PUT` | `/api/{ns}/{key}` | write `ns` | Update an entry (`404` if missing) |
103
- | `DELETE` | `/api/{ns}/{key}` | write `ns` | Delete an entry |
104
- | `DELETE` | `/api/{ns}` | write `ns` | Delete a namespace |
98
+ | Method | Path | Permission | Description |
99
+ |----------|-----------------------------------|------------|---------------------------------|
100
+ | `GET` | `/api/health` | — | Health + auth status |
101
+ | `GET` | `/api/whoami` | — | Caller's access summary |
102
+ | `GET` | `/api/namespaces` | any key | List accessible namespaces |
103
+ | `POST` | `/api/namespaces` | admin | Create a namespace |
104
+ | `DELETE` | `/api/{ns}` | write `ns` | Delete a namespace |
105
+ | `GET` | `/api/{ns}` | read `ns` | List tables |
106
+ | `POST` | `/api/{ns}` | write `ns` | Create a table (`{"name": …}`) |
107
+ | `GET` | `/api/{ns}/{table}` | read `ns` | Get a table's designed columns |
108
+ | `PUT` | `/api/{ns}/{table}` | write `ns` | Design columns (`{"columns":[…]}`) |
109
+ | `DELETE` | `/api/{ns}/{table}` | write `ns` | Delete a table |
110
+ | `GET` | `/api/{ns}/{table}/rows` | read `ns` | List rows (filter with `?col=value`) |
111
+ | `POST` | `/api/{ns}/{table}/rows` | write `ns` | Create a row (body = its fields)|
112
+ | `GET` | `/api/{ns}/{table}/rows/{id}` | read `ns` | Get one row |
113
+ | `PUT` | `/api/{ns}/{table}/rows/{id}` | write `ns` | Replace a row's fields |
114
+ | `DELETE` | `/api/{ns}/{table}/rows/{id}` | write `ns` | Delete a row |
115
+
116
+ In the web UI, open a table and use the **Columns** designer to add/remove
117
+ columns; they persist and appear in the grid and the add-row form even with no
118
+ rows yet.
105
119
 
106
120
  ```bash
107
- # create — POST to the namespace, key in the body
108
- curl -X POST localhost:8000/api/demo \
109
- -H 'Content-Type: application/json' \
110
- -d '{"key":"greeting","value":"hello","type":"string"}'
121
+ curl -X POST localhost:8000/api/namespaces -H 'Content-Type: application/json' -d '{"name":"demo"}'
122
+ curl -X POST localhost:8000/api/demo -H 'Content-Type: application/json' -d '{"name":"users"}'
111
123
 
112
- # update — PUT to the entry's URI
113
- curl -X PUT localhost:8000/api/demo/greeting \
124
+ # a row IS its fields — any shape you like
125
+ curl -X POST localhost:8000/api/demo/users/rows \
114
126
  -H 'Content-Type: application/json' \
115
- -d '{"value":"hi there","type":"string"}'
127
+ -d '{"name":"alice","age":30,"active":true}'
128
+ # -> {"id":"<row-id>","updated_at":"...","fields":{...}}
116
129
 
117
- curl localhost:8000/api/demo # list entries
130
+ curl localhost:8000/api/demo/users/rows
131
+ # -> {"columns":["name","age","active", ...], "rows":[{"id":..., "fields":{...}}, ...]}
132
+
133
+ # filter by column conditions — each query param is a column=value test,
134
+ # and multiple are AND-ed together:
135
+ curl "localhost:8000/api/demo/users/rows?status=ok&ean=123"
118
136
  ```
119
137
 
120
- (The names `namespaces`, `health`, and `whoami` are reserved and can't be used
121
- as namespaces, since they'd collide with the routes above.)
138
+ Filter values are matched leniently: `ean=123` matches a numeric `123` or the
139
+ string `"123"`, `active=true` matches the boolean. The web UI has a matching
140
+ **filter** box on each table (type `status=ok&ean=123`).
141
+
142
+ (The names `namespaces`, `health`, `whoami`, `keys`, `accounts`, `login`,
143
+ `logout`, `password`, and `me` are reserved and can't be used as namespaces.)
122
144
 
123
- The API is self-documenting via OpenAPI. Interactive docs are available at:
145
+ The API is self-documenting via OpenAPI:
124
146
 
125
147
  - **`/docs`** — Swagger UI (also linked as **API docs** in the web interface header)
126
148
  - **`/redoc`** — ReDoc
@@ -129,24 +151,20 @@ The API is self-documenting via OpenAPI. Interactive docs are available at:
129
151
  When auth is enabled, each operation in Swagger exposes an `X-API-Key` header
130
152
  field you can fill in to try requests.
131
153
 
132
- ### CSV import / export (web UI only)
154
+ ### CSV import / export (web UI only, per table)
133
155
 
134
- The web interface has **Download CSV** / **Import CSV** buttons on each
135
- namespace. These are a convenience of the web UI only — they are built on the
136
- regular entry API (export reads the entries; import creates/updates each row),
137
- so there are no dedicated CSV endpoints. The CSV has three columns —
138
- `key`, `type`, `value`:
156
+ Each table in the web UI has **Download CSV** / **Import CSV** buttons. A table
157
+ maps naturally to CSV: the header row is the column names, each line is a row.
158
+ These are built on the rows API (no dedicated CSV endpoints). On import, each
159
+ cell is parsed leniently — `30` → number, `true` → boolean, `[1,2]` → array,
160
+ anything else → string.
139
161
 
140
162
  ```csv
141
- key,type,value
142
- name,string,alice
143
- count,number,7
144
- tags,array,"[""x"", ""y""]"
163
+ name,age,active
164
+ alice,30,true
165
+ bob,25,false
145
166
  ```
146
167
 
147
- On import the `type` column is optional — if omitted, each value's type is
148
- inferred from its text.
149
-
150
168
  ### Access control (accounts + API keys)
151
169
 
152
170
  Off by default. **Turn it on** with `auth: true` in `tds.yml`, `--auth`, or
@@ -182,9 +200,9 @@ Each signed-in user can mint a personal **API key** (web UI → *My API keys*, o
182
200
 
183
201
  ```bash
184
202
  # the token is returned once; then use it as a header
185
- curl -X POST localhost:8000/api/projects \
203
+ curl -X POST localhost:8000/api/projects/people/rows \
186
204
  -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
187
- -d '{"key":"env","value":"staging","type":"string"}'
205
+ -d '{"name":"alice","role":"qa"}'
188
206
  ```
189
207
 
190
208
  #### Auth & admin endpoints (summary)
@@ -211,7 +229,9 @@ environment variables:
211
229
 
212
230
  | YAML key | Env var | Default | Purpose |
213
231
  |------------|--------------------|--------------|------------------------------------------|
214
- | `data_dir` | `TDS_DATA_DIR` | `./tds-data` | Namespace files + `.auth/` live here |
232
+ | `data_dir` | `TDS_DATA_DIR` | `./tds-data` | Data + `.auth/` live here |
233
+ | `storage` | `TDS_STORAGE` | `sqlite` | Storage backend: `sqlite` or `file` |
234
+ | `db_path` | `TDS_DB_PATH` | `<data_dir>/tds.db` | SQLite DB path (sqlite backend) |
215
235
  | `auth` | `TDS_AUTH_ENABLED` | `false` | Turn access control on/off |
216
236
  | `host` | — | `127.0.0.1` | Bind host |
217
237
  | `port` | — | `8000` | Bind port |
@@ -220,8 +240,17 @@ environment variables:
220
240
  | `api_keys` | `TDS_API_KEYS` | *(empty)* | Seed machine keys + grants |
221
241
  | — | `TDS_KEY_PEPPER` | *(empty)* | Optional pepper for API-key hashing |
222
242
 
223
- Credentials live under `<data_dir>/.auth/` (`accounts.json`, `keys.json`,
224
- `secret`) — all hashed/secret material, never plaintext passwords or tokens.
243
+ #### Storage backend
244
+
245
+ Data is kept in **SQLite** by default — a single `tds.db` file the app creates
246
+ in `data_dir` on first start (no setup, no server). Rows store their dynamic
247
+ fields in a JSON column, so the schema is fixed while the data stays free-form.
248
+ Set `storage: file` to use one JSON file per namespace instead (human-readable
249
+ and dependency-free, but it rewrites a whole file per change and isn't safe
250
+ across processes). Either way, point `data_dir` at a persistent volume.
251
+
252
+ Credentials always live under `<data_dir>/.auth/` (`accounts.json`,
253
+ `keys.json`, `secret`) — hashed/secret material, never plaintext.
225
254
 
226
255
  ### Tests
227
256
 
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "test-data-service"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "A disk-backed test data service with a REST API and web interface"
9
9
  readme = "readme.md"
10
10
  requires-python = ">=3.10"
11
11
  license = { text = "MIT" }
12
12
  authors = [
13
- { name = "Roy de Kleijn", email = "dekleijn.roy@gmail.com" },
13
+ { name = "Roy de Kleijn", email = "info@testsmith.io" },
14
14
  ]
15
15
  keywords = ["test-data", "rest", "api", "fastapi", "key-value", "testing", "mock-data"]
16
16
  classifiers = [
@@ -43,46 +43,68 @@ port: 8000
43
43
 
44
44
  ### Data model
45
45
 
46
- Each entry is a `key` / `value` pair inside a **namespace**. A value is one
47
- of four types: `string`, `boolean`, `number`, or `array`. The type is stored
48
- alongside the value and validated on write.
46
+ Three levels: **namespace → table → row**.
47
+
48
+ * A **namespace** is the unit of access control.
49
+ * A namespace holds any number of **tables**.
50
+ * A table holds **rows**, and each row is a record with an **arbitrary set of
51
+ fields**. Field values can be any JSON (string, number, boolean, array,
52
+ object, null).
53
+ * A table can also have **designed columns** — a persisted column list you set
54
+ up front (the "schema"), which shows even before any rows exist. The
55
+ *effective* columns of a table are its designed columns plus any extra fields
56
+ that appear in its rows, so the layout stays dynamic.
49
57
 
50
58
  ### REST API
51
59
 
52
- `POST` targets the namespace (collection) with the key **in the body**; `PUT`
53
- targets a specific entry URI. They are not interchangeable — `POST` only creates,
54
- `PUT` only updates.
55
-
56
- | Method | Path | Permission | Description |
57
- |----------|-------------------------|------------|--------------------------------------|
58
- | `GET` | `/api/health` | — | Health + auth status |
59
- | `GET` | `/api/whoami` | — | Caller's access summary |
60
- | `GET` | `/api/namespaces` | any key | List accessible namespaces |
61
- | `GET` | `/api/{ns}` | read `ns` | List entries in a namespace |
62
- | `POST` | `/api/{ns}` | write `ns` | Create an entry (`409` if key exists) |
63
- | `GET` | `/api/{ns}/{key}` | read `ns` | Get one entry |
64
- | `PUT` | `/api/{ns}/{key}` | write `ns` | Update an entry (`404` if missing) |
65
- | `DELETE` | `/api/{ns}/{key}` | write `ns` | Delete an entry |
66
- | `DELETE` | `/api/{ns}` | write `ns` | Delete a namespace |
60
+ | Method | Path | Permission | Description |
61
+ |----------|-----------------------------------|------------|---------------------------------|
62
+ | `GET` | `/api/health` | — | Health + auth status |
63
+ | `GET` | `/api/whoami` | — | Caller's access summary |
64
+ | `GET` | `/api/namespaces` | any key | List accessible namespaces |
65
+ | `POST` | `/api/namespaces` | admin | Create a namespace |
66
+ | `DELETE` | `/api/{ns}` | write `ns` | Delete a namespace |
67
+ | `GET` | `/api/{ns}` | read `ns` | List tables |
68
+ | `POST` | `/api/{ns}` | write `ns` | Create a table (`{"name": …}`) |
69
+ | `GET` | `/api/{ns}/{table}` | read `ns` | Get a table's designed columns |
70
+ | `PUT` | `/api/{ns}/{table}` | write `ns` | Design columns (`{"columns":[…]}`) |
71
+ | `DELETE` | `/api/{ns}/{table}` | write `ns` | Delete a table |
72
+ | `GET` | `/api/{ns}/{table}/rows` | read `ns` | List rows (filter with `?col=value`) |
73
+ | `POST` | `/api/{ns}/{table}/rows` | write `ns` | Create a row (body = its fields)|
74
+ | `GET` | `/api/{ns}/{table}/rows/{id}` | read `ns` | Get one row |
75
+ | `PUT` | `/api/{ns}/{table}/rows/{id}` | write `ns` | Replace a row's fields |
76
+ | `DELETE` | `/api/{ns}/{table}/rows/{id}` | write `ns` | Delete a row |
77
+
78
+ In the web UI, open a table and use the **Columns** designer to add/remove
79
+ columns; they persist and appear in the grid and the add-row form even with no
80
+ rows yet.
67
81
 
68
82
  ```bash
69
- # create — POST to the namespace, key in the body
70
- curl -X POST localhost:8000/api/demo \
71
- -H 'Content-Type: application/json' \
72
- -d '{"key":"greeting","value":"hello","type":"string"}'
83
+ curl -X POST localhost:8000/api/namespaces -H 'Content-Type: application/json' -d '{"name":"demo"}'
84
+ curl -X POST localhost:8000/api/demo -H 'Content-Type: application/json' -d '{"name":"users"}'
73
85
 
74
- # update — PUT to the entry's URI
75
- curl -X PUT localhost:8000/api/demo/greeting \
86
+ # a row IS its fields — any shape you like
87
+ curl -X POST localhost:8000/api/demo/users/rows \
76
88
  -H 'Content-Type: application/json' \
77
- -d '{"value":"hi there","type":"string"}'
89
+ -d '{"name":"alice","age":30,"active":true}'
90
+ # -> {"id":"<row-id>","updated_at":"...","fields":{...}}
91
+
92
+ curl localhost:8000/api/demo/users/rows
93
+ # -> {"columns":["name","age","active", ...], "rows":[{"id":..., "fields":{...}}, ...]}
78
94
 
79
- curl localhost:8000/api/demo # list entries
95
+ # filter by column conditions — each query param is a column=value test,
96
+ # and multiple are AND-ed together:
97
+ curl "localhost:8000/api/demo/users/rows?status=ok&ean=123"
80
98
  ```
81
99
 
82
- (The names `namespaces`, `health`, and `whoami` are reserved and can't be used
83
- as namespaces, since they'd collide with the routes above.)
100
+ Filter values are matched leniently: `ean=123` matches a numeric `123` or the
101
+ string `"123"`, `active=true` matches the boolean. The web UI has a matching
102
+ **filter** box on each table (type `status=ok&ean=123`).
103
+
104
+ (The names `namespaces`, `health`, `whoami`, `keys`, `accounts`, `login`,
105
+ `logout`, `password`, and `me` are reserved and can't be used as namespaces.)
84
106
 
85
- The API is self-documenting via OpenAPI. Interactive docs are available at:
107
+ The API is self-documenting via OpenAPI:
86
108
 
87
109
  - **`/docs`** — Swagger UI (also linked as **API docs** in the web interface header)
88
110
  - **`/redoc`** — ReDoc
@@ -91,24 +113,20 @@ The API is self-documenting via OpenAPI. Interactive docs are available at:
91
113
  When auth is enabled, each operation in Swagger exposes an `X-API-Key` header
92
114
  field you can fill in to try requests.
93
115
 
94
- ### CSV import / export (web UI only)
116
+ ### CSV import / export (web UI only, per table)
95
117
 
96
- The web interface has **Download CSV** / **Import CSV** buttons on each
97
- namespace. These are a convenience of the web UI only — they are built on the
98
- regular entry API (export reads the entries; import creates/updates each row),
99
- so there are no dedicated CSV endpoints. The CSV has three columns —
100
- `key`, `type`, `value`:
118
+ Each table in the web UI has **Download CSV** / **Import CSV** buttons. A table
119
+ maps naturally to CSV: the header row is the column names, each line is a row.
120
+ These are built on the rows API (no dedicated CSV endpoints). On import, each
121
+ cell is parsed leniently — `30` → number, `true` → boolean, `[1,2]` → array,
122
+ anything else → string.
101
123
 
102
124
  ```csv
103
- key,type,value
104
- name,string,alice
105
- count,number,7
106
- tags,array,"[""x"", ""y""]"
125
+ name,age,active
126
+ alice,30,true
127
+ bob,25,false
107
128
  ```
108
129
 
109
- On import the `type` column is optional — if omitted, each value's type is
110
- inferred from its text.
111
-
112
130
  ### Access control (accounts + API keys)
113
131
 
114
132
  Off by default. **Turn it on** with `auth: true` in `tds.yml`, `--auth`, or
@@ -144,9 +162,9 @@ Each signed-in user can mint a personal **API key** (web UI → *My API keys*, o
144
162
 
145
163
  ```bash
146
164
  # the token is returned once; then use it as a header
147
- curl -X POST localhost:8000/api/projects \
165
+ curl -X POST localhost:8000/api/projects/people/rows \
148
166
  -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
149
- -d '{"key":"env","value":"staging","type":"string"}'
167
+ -d '{"name":"alice","role":"qa"}'
150
168
  ```
151
169
 
152
170
  #### Auth & admin endpoints (summary)
@@ -173,7 +191,9 @@ environment variables:
173
191
 
174
192
  | YAML key | Env var | Default | Purpose |
175
193
  |------------|--------------------|--------------|------------------------------------------|
176
- | `data_dir` | `TDS_DATA_DIR` | `./tds-data` | Namespace files + `.auth/` live here |
194
+ | `data_dir` | `TDS_DATA_DIR` | `./tds-data` | Data + `.auth/` live here |
195
+ | `storage` | `TDS_STORAGE` | `sqlite` | Storage backend: `sqlite` or `file` |
196
+ | `db_path` | `TDS_DB_PATH` | `<data_dir>/tds.db` | SQLite DB path (sqlite backend) |
177
197
  | `auth` | `TDS_AUTH_ENABLED` | `false` | Turn access control on/off |
178
198
  | `host` | — | `127.0.0.1` | Bind host |
179
199
  | `port` | — | `8000` | Bind port |
@@ -182,8 +202,17 @@ environment variables:
182
202
  | `api_keys` | `TDS_API_KEYS` | *(empty)* | Seed machine keys + grants |
183
203
  | — | `TDS_KEY_PEPPER` | *(empty)* | Optional pepper for API-key hashing |
184
204
 
185
- Credentials live under `<data_dir>/.auth/` (`accounts.json`, `keys.json`,
186
- `secret`) — all hashed/secret material, never plaintext passwords or tokens.
205
+ #### Storage backend
206
+
207
+ Data is kept in **SQLite** by default — a single `tds.db` file the app creates
208
+ in `data_dir` on first start (no setup, no server). Rows store their dynamic
209
+ fields in a JSON column, so the schema is fixed while the data stays free-form.
210
+ Set `storage: file` to use one JSON file per namespace instead (human-readable
211
+ and dependency-free, but it rewrites a whole file per change and isn't safe
212
+ across processes). Either way, point `data_dir` at a persistent volume.
213
+
214
+ Credentials always live under `<data_dir>/.auth/` (`accounts.json`,
215
+ `keys.json`, `secret`) — hashed/secret material, never plaintext.
187
216
 
188
217
  ### Tests
189
218
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: test-data-service
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A disk-backed test data service with a REST API and web interface
5
- Author-email: Roy de Kleijn <dekleijn.roy@gmail.com>
5
+ Author-email: Roy de Kleijn <info@testsmith.io>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/testsmith-io/test-data-service
8
8
  Project-URL: Documentation, https://github.com/testsmith-io/test-data-service#readme
@@ -81,46 +81,68 @@ port: 8000
81
81
 
82
82
  ### Data model
83
83
 
84
- Each entry is a `key` / `value` pair inside a **namespace**. A value is one
85
- of four types: `string`, `boolean`, `number`, or `array`. The type is stored
86
- alongside the value and validated on write.
84
+ Three levels: **namespace → table → row**.
85
+
86
+ * A **namespace** is the unit of access control.
87
+ * A namespace holds any number of **tables**.
88
+ * A table holds **rows**, and each row is a record with an **arbitrary set of
89
+ fields**. Field values can be any JSON (string, number, boolean, array,
90
+ object, null).
91
+ * A table can also have **designed columns** — a persisted column list you set
92
+ up front (the "schema"), which shows even before any rows exist. The
93
+ *effective* columns of a table are its designed columns plus any extra fields
94
+ that appear in its rows, so the layout stays dynamic.
87
95
 
88
96
  ### REST API
89
97
 
90
- `POST` targets the namespace (collection) with the key **in the body**; `PUT`
91
- targets a specific entry URI. They are not interchangeable — `POST` only creates,
92
- `PUT` only updates.
93
-
94
- | Method | Path | Permission | Description |
95
- |----------|-------------------------|------------|--------------------------------------|
96
- | `GET` | `/api/health` | — | Health + auth status |
97
- | `GET` | `/api/whoami` | — | Caller's access summary |
98
- | `GET` | `/api/namespaces` | any key | List accessible namespaces |
99
- | `GET` | `/api/{ns}` | read `ns` | List entries in a namespace |
100
- | `POST` | `/api/{ns}` | write `ns` | Create an entry (`409` if key exists) |
101
- | `GET` | `/api/{ns}/{key}` | read `ns` | Get one entry |
102
- | `PUT` | `/api/{ns}/{key}` | write `ns` | Update an entry (`404` if missing) |
103
- | `DELETE` | `/api/{ns}/{key}` | write `ns` | Delete an entry |
104
- | `DELETE` | `/api/{ns}` | write `ns` | Delete a namespace |
98
+ | Method | Path | Permission | Description |
99
+ |----------|-----------------------------------|------------|---------------------------------|
100
+ | `GET` | `/api/health` | — | Health + auth status |
101
+ | `GET` | `/api/whoami` | — | Caller's access summary |
102
+ | `GET` | `/api/namespaces` | any key | List accessible namespaces |
103
+ | `POST` | `/api/namespaces` | admin | Create a namespace |
104
+ | `DELETE` | `/api/{ns}` | write `ns` | Delete a namespace |
105
+ | `GET` | `/api/{ns}` | read `ns` | List tables |
106
+ | `POST` | `/api/{ns}` | write `ns` | Create a table (`{"name": …}`) |
107
+ | `GET` | `/api/{ns}/{table}` | read `ns` | Get a table's designed columns |
108
+ | `PUT` | `/api/{ns}/{table}` | write `ns` | Design columns (`{"columns":[…]}`) |
109
+ | `DELETE` | `/api/{ns}/{table}` | write `ns` | Delete a table |
110
+ | `GET` | `/api/{ns}/{table}/rows` | read `ns` | List rows (filter with `?col=value`) |
111
+ | `POST` | `/api/{ns}/{table}/rows` | write `ns` | Create a row (body = its fields)|
112
+ | `GET` | `/api/{ns}/{table}/rows/{id}` | read `ns` | Get one row |
113
+ | `PUT` | `/api/{ns}/{table}/rows/{id}` | write `ns` | Replace a row's fields |
114
+ | `DELETE` | `/api/{ns}/{table}/rows/{id}` | write `ns` | Delete a row |
115
+
116
+ In the web UI, open a table and use the **Columns** designer to add/remove
117
+ columns; they persist and appear in the grid and the add-row form even with no
118
+ rows yet.
105
119
 
106
120
  ```bash
107
- # create — POST to the namespace, key in the body
108
- curl -X POST localhost:8000/api/demo \
109
- -H 'Content-Type: application/json' \
110
- -d '{"key":"greeting","value":"hello","type":"string"}'
121
+ curl -X POST localhost:8000/api/namespaces -H 'Content-Type: application/json' -d '{"name":"demo"}'
122
+ curl -X POST localhost:8000/api/demo -H 'Content-Type: application/json' -d '{"name":"users"}'
111
123
 
112
- # update — PUT to the entry's URI
113
- curl -X PUT localhost:8000/api/demo/greeting \
124
+ # a row IS its fields — any shape you like
125
+ curl -X POST localhost:8000/api/demo/users/rows \
114
126
  -H 'Content-Type: application/json' \
115
- -d '{"value":"hi there","type":"string"}'
127
+ -d '{"name":"alice","age":30,"active":true}'
128
+ # -> {"id":"<row-id>","updated_at":"...","fields":{...}}
116
129
 
117
- curl localhost:8000/api/demo # list entries
130
+ curl localhost:8000/api/demo/users/rows
131
+ # -> {"columns":["name","age","active", ...], "rows":[{"id":..., "fields":{...}}, ...]}
132
+
133
+ # filter by column conditions — each query param is a column=value test,
134
+ # and multiple are AND-ed together:
135
+ curl "localhost:8000/api/demo/users/rows?status=ok&ean=123"
118
136
  ```
119
137
 
120
- (The names `namespaces`, `health`, and `whoami` are reserved and can't be used
121
- as namespaces, since they'd collide with the routes above.)
138
+ Filter values are matched leniently: `ean=123` matches a numeric `123` or the
139
+ string `"123"`, `active=true` matches the boolean. The web UI has a matching
140
+ **filter** box on each table (type `status=ok&ean=123`).
141
+
142
+ (The names `namespaces`, `health`, `whoami`, `keys`, `accounts`, `login`,
143
+ `logout`, `password`, and `me` are reserved and can't be used as namespaces.)
122
144
 
123
- The API is self-documenting via OpenAPI. Interactive docs are available at:
145
+ The API is self-documenting via OpenAPI:
124
146
 
125
147
  - **`/docs`** — Swagger UI (also linked as **API docs** in the web interface header)
126
148
  - **`/redoc`** — ReDoc
@@ -129,24 +151,20 @@ The API is self-documenting via OpenAPI. Interactive docs are available at:
129
151
  When auth is enabled, each operation in Swagger exposes an `X-API-Key` header
130
152
  field you can fill in to try requests.
131
153
 
132
- ### CSV import / export (web UI only)
154
+ ### CSV import / export (web UI only, per table)
133
155
 
134
- The web interface has **Download CSV** / **Import CSV** buttons on each
135
- namespace. These are a convenience of the web UI only — they are built on the
136
- regular entry API (export reads the entries; import creates/updates each row),
137
- so there are no dedicated CSV endpoints. The CSV has three columns —
138
- `key`, `type`, `value`:
156
+ Each table in the web UI has **Download CSV** / **Import CSV** buttons. A table
157
+ maps naturally to CSV: the header row is the column names, each line is a row.
158
+ These are built on the rows API (no dedicated CSV endpoints). On import, each
159
+ cell is parsed leniently — `30` → number, `true` → boolean, `[1,2]` → array,
160
+ anything else → string.
139
161
 
140
162
  ```csv
141
- key,type,value
142
- name,string,alice
143
- count,number,7
144
- tags,array,"[""x"", ""y""]"
163
+ name,age,active
164
+ alice,30,true
165
+ bob,25,false
145
166
  ```
146
167
 
147
- On import the `type` column is optional — if omitted, each value's type is
148
- inferred from its text.
149
-
150
168
  ### Access control (accounts + API keys)
151
169
 
152
170
  Off by default. **Turn it on** with `auth: true` in `tds.yml`, `--auth`, or
@@ -182,9 +200,9 @@ Each signed-in user can mint a personal **API key** (web UI → *My API keys*, o
182
200
 
183
201
  ```bash
184
202
  # the token is returned once; then use it as a header
185
- curl -X POST localhost:8000/api/projects \
203
+ curl -X POST localhost:8000/api/projects/people/rows \
186
204
  -H "X-API-Key: $TOKEN" -H "Content-Type: application/json" \
187
- -d '{"key":"env","value":"staging","type":"string"}'
205
+ -d '{"name":"alice","role":"qa"}'
188
206
  ```
189
207
 
190
208
  #### Auth & admin endpoints (summary)
@@ -211,7 +229,9 @@ environment variables:
211
229
 
212
230
  | YAML key | Env var | Default | Purpose |
213
231
  |------------|--------------------|--------------|------------------------------------------|
214
- | `data_dir` | `TDS_DATA_DIR` | `./tds-data` | Namespace files + `.auth/` live here |
232
+ | `data_dir` | `TDS_DATA_DIR` | `./tds-data` | Data + `.auth/` live here |
233
+ | `storage` | `TDS_STORAGE` | `sqlite` | Storage backend: `sqlite` or `file` |
234
+ | `db_path` | `TDS_DB_PATH` | `<data_dir>/tds.db` | SQLite DB path (sqlite backend) |
215
235
  | `auth` | `TDS_AUTH_ENABLED` | `false` | Turn access control on/off |
216
236
  | `host` | — | `127.0.0.1` | Bind host |
217
237
  | `port` | — | `8000` | Bind port |
@@ -220,8 +240,17 @@ environment variables:
220
240
  | `api_keys` | `TDS_API_KEYS` | *(empty)* | Seed machine keys + grants |
221
241
  | — | `TDS_KEY_PEPPER` | *(empty)* | Optional pepper for API-key hashing |
222
242
 
223
- Credentials live under `<data_dir>/.auth/` (`accounts.json`, `keys.json`,
224
- `secret`) — all hashed/secret material, never plaintext passwords or tokens.
243
+ #### Storage backend
244
+
245
+ Data is kept in **SQLite** by default — a single `tds.db` file the app creates
246
+ in `data_dir` on first start (no setup, no server). Rows store their dynamic
247
+ fields in a JSON column, so the schema is fixed while the data stays free-form.
248
+ Set `storage: file` to use one JSON file per namespace instead (human-readable
249
+ and dependency-free, but it rewrites a whole file per change and isn't safe
250
+ across processes). Either way, point `data_dir` at a persistent volume.
251
+
252
+ Credentials always live under `<data_dir>/.auth/` (`accounts.json`,
253
+ `keys.json`, `secret`) — hashed/secret material, never plaintext.
225
254
 
226
255
  ### Tests
227
256
 
@@ -22,6 +22,7 @@ src/testdataservice/config.py
22
22
  src/testdataservice/keystore.py
23
23
  src/testdataservice/passwords.py
24
24
  src/testdataservice/sessions.py
25
+ src/testdataservice/sqlite_storage.py
25
26
  src/testdataservice/storage.py
26
27
  src/testdataservice/types.py
27
28
  src/testdataservice/web/static/app.js
@@ -0,0 +1,36 @@
1
+ """Test Data Service — a disk-backed namespace/table/row store with an API and web UI."""
2
+
3
+ from .app import create_app
4
+ from .config import Config
5
+ from .storage import (
6
+ Storage,
7
+ FileStorage,
8
+ StorageBackend,
9
+ open_storage,
10
+ Row,
11
+ NamespaceNotFoundError,
12
+ TableNotFoundError,
13
+ TableExistsError,
14
+ RowNotFoundError,
15
+ InvalidNamespaceError,
16
+ InvalidTableError,
17
+ )
18
+ from .sqlite_storage import SqliteStorage
19
+
20
+ __version__ = "0.1.0"
21
+ __all__ = [
22
+ "create_app",
23
+ "Config",
24
+ "Storage",
25
+ "FileStorage",
26
+ "SqliteStorage",
27
+ "StorageBackend",
28
+ "open_storage",
29
+ "Row",
30
+ "NamespaceNotFoundError",
31
+ "TableNotFoundError",
32
+ "TableExistsError",
33
+ "RowNotFoundError",
34
+ "InvalidNamespaceError",
35
+ "InvalidTableError",
36
+ ]