M2-T04: add single-row record CRUD API (patch/delete)

- PATCH/DELETE /api/locations/{person}/{datetime} and /api/poo/{timestamp}
- update only non-PK fields (PK immutable); 404 on missing PK
- delete scoped to exact full PK with rowcount guard (0->404, 1->ok);
  no batch/truncate/drop path
- session + CSRF protected; bare ingestion endpoints untouched
- service helpers in app/services/location.py and poo.py; regenerate openapi/
- tests/test_api_record_crud.py
This commit is contained in:
2026-06-12 23:33:08 +02:00
parent 9ce3f2a0b8
commit 048414c5cb
7 changed files with 1377 additions and 4 deletions
+336
View File
@@ -542,6 +542,262 @@
}
}
},
"/api/locations/{person}/{datetime}": {
"patch": {
"tags": [
"api-data"
],
"summary": "Patch Location",
"description": "Update the non-PK fields of a single location record.\n\n- ``person`` and ``datetime`` identify the row (composite PK) and are immutable.\n- Only ``latitude``, ``longitude``, and ``altitude`` may be updated.\n- Omitted body fields are left unchanged.\n- Returns **404** if the PK does not exist.",
"operationId": "patch_location_api_locations__person___datetime__patch",
"parameters": [
{
"name": "person",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Person"
}
},
{
"name": "datetime",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Datetime"
}
},
{
"name": "X-CSRF-Token",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "X-Csrf-Token"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LocationUpdateRequest",
"default": {}
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LocationRecord"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"delete": {
"tags": [
"api-data"
],
"summary": "Delete Location Record",
"description": "Delete the single location record identified by its composite PK.\n\n- Exactly one row is deleted; **404** if the PK does not exist.\n- No batch delete / truncate path is available.",
"operationId": "delete_location_record_api_locations__person___datetime__delete",
"parameters": [
{
"name": "person",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Person"
}
},
{
"name": "datetime",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Datetime"
}
},
{
"name": "X-CSRF-Token",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "X-Csrf-Token"
}
}
],
"responses": {
"204": {
"description": "Successful Response"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/poo/{timestamp}": {
"patch": {
"tags": [
"api-data"
],
"summary": "Patch Poo",
"description": "Update the non-PK fields of a single poo record.\n\n- ``timestamp`` is the PK and is immutable.\n- Only ``status``, ``latitude``, and ``longitude`` may be updated.\n- Omitted body fields are left unchanged.\n- Returns **404** if the PK does not exist.",
"operationId": "patch_poo_api_poo__timestamp__patch",
"parameters": [
{
"name": "timestamp",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Timestamp"
}
},
{
"name": "X-CSRF-Token",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "X-Csrf-Token"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PooUpdateRequest",
"default": {}
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PooRecord"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"delete": {
"tags": [
"api-data"
],
"summary": "Delete Poo",
"description": "Delete the single poo record identified by its PK.\n\n- Exactly one row is deleted; **404** if the PK does not exist.\n- No batch delete / truncate path is available.",
"operationId": "delete_poo_api_poo__timestamp__delete",
"parameters": [
{
"name": "timestamp",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Timestamp"
}
},
{
"name": "X-CSRF-Token",
"in": "header",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "X-Csrf-Token"
}
}
],
"responses": {
"204": {
"description": "Successful Response"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/session": {
"get": {
"tags": [
@@ -1075,6 +1331,46 @@
],
"title": "LocationRecord"
},
"LocationUpdateRequest": {
"properties": {
"latitude": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Longitude"
},
"altitude": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Altitude"
}
},
"type": "object",
"title": "LocationUpdateRequest",
"description": "PATCH body for a location record — all fields optional; PK fields excluded."
},
"LocationsResponse": {
"properties": {
"items": {
@@ -1196,6 +1492,46 @@
],
"title": "PooResponse"
},
"PooUpdateRequest": {
"properties": {
"status": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Status"
},
"latitude": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Latitude"
},
"longitude": {
"anyOf": [
{
"type": "number"
},
{
"type": "null"
}
],
"title": "Longitude"
}
},
"type": "object",
"title": "PooUpdateRequest",
"description": "PATCH body for a poo record — all fields optional; PK field excluded."
},
"PublicIPCheckResponse": {
"properties": {
"status": {
+224
View File
@@ -364,6 +364,189 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/locations/{person}/{datetime}:
patch:
tags:
- api-data
summary: Patch Location
description: 'Update the non-PK fields of a single location record.
- ``person`` and ``datetime`` identify the row (composite PK) and are immutable.
- Only ``latitude``, ``longitude``, and ``altitude`` may be updated.
- Omitted body fields are left unchanged.
- Returns **404** if the PK does not exist.'
operationId: patch_location_api_locations__person___datetime__patch
parameters:
- name: person
in: path
required: true
schema:
type: string
title: Person
- name: datetime
in: path
required: true
schema:
type: string
title: Datetime
- name: X-CSRF-Token
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: X-Csrf-Token
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LocationUpdateRequest'
default: {}
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/LocationRecord'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
delete:
tags:
- api-data
summary: Delete Location Record
description: 'Delete the single location record identified by its composite
PK.
- Exactly one row is deleted; **404** if the PK does not exist.
- No batch delete / truncate path is available.'
operationId: delete_location_record_api_locations__person___datetime__delete
parameters:
- name: person
in: path
required: true
schema:
type: string
title: Person
- name: datetime
in: path
required: true
schema:
type: string
title: Datetime
- name: X-CSRF-Token
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: X-Csrf-Token
responses:
'204':
description: Successful Response
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/poo/{timestamp}:
patch:
tags:
- api-data
summary: Patch Poo
description: 'Update the non-PK fields of a single poo record.
- ``timestamp`` is the PK and is immutable.
- Only ``status``, ``latitude``, and ``longitude`` may be updated.
- Omitted body fields are left unchanged.
- Returns **404** if the PK does not exist.'
operationId: patch_poo_api_poo__timestamp__patch
parameters:
- name: timestamp
in: path
required: true
schema:
type: string
title: Timestamp
- name: X-CSRF-Token
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: X-Csrf-Token
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PooUpdateRequest'
default: {}
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/PooRecord'
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
delete:
tags:
- api-data
summary: Delete Poo
description: 'Delete the single poo record identified by its PK.
- Exactly one row is deleted; **404** if the PK does not exist.
- No batch delete / truncate path is available.'
operationId: delete_poo_api_poo__timestamp__delete
parameters:
- name: timestamp
in: path
required: true
schema:
type: string
title: Timestamp
- name: X-CSRF-Token
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: X-Csrf-Token
responses:
'204':
description: Successful Response
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
/api/session:
get:
tags:
@@ -735,6 +918,27 @@ components:
- longitude
- altitude
title: LocationRecord
LocationUpdateRequest:
properties:
latitude:
anyOf:
- type: number
- type: 'null'
title: Latitude
longitude:
anyOf:
- type: number
- type: 'null'
title: Longitude
altitude:
anyOf:
- type: number
- type: 'null'
title: Altitude
type: object
title: LocationUpdateRequest
description: PATCH body for a location record — all fields optional; PK fields
excluded.
LocationsResponse:
properties:
items:
@@ -824,6 +1028,26 @@ components:
- limit
- offset
title: PooResponse
PooUpdateRequest:
properties:
status:
anyOf:
- type: string
- type: 'null'
title: Status
latitude:
anyOf:
- type: number
- type: 'null'
title: Latitude
longitude:
anyOf:
- type: number
- type: 'null'
title: Longitude
type: object
title: PooUpdateRequest
description: PATCH body for a poo record — all fields optional; PK field excluded.
PublicIPCheckResponse:
properties:
status: