servicekit 2.0.0__tar.gz → 2.0.1__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.
- {servicekit-2.0.0 → servicekit-2.0.1}/PKG-INFO +5 -4
- {servicekit-2.0.0 → servicekit-2.0.1}/README.md +4 -3
- {servicekit-2.0.0 → servicekit-2.0.1}/pyproject.toml +1 -1
- {servicekit-2.0.0 → servicekit-2.0.1}/pyproject.toml.orig +1 -1
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/__init__.py +2 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/__init__.py +2 -1
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/crud.py +25 -5
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/middleware.py +4 -1
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/routers/job.py +26 -14
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/service_builder.py +8 -4
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/exceptions.py +15 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/manager.py +6 -2
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/alembic/env.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/alembic/script.py.mako +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/alembic/versions/20251010_0927_4d869b5fb06e_initial_schema.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/app.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/apps/landing/index.html +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/apps/landing/manifest.json +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/auth.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/dependencies.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/monitoring.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/pagination.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/registration.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/router.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/routers/__init__.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/routers/health.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/routers/metrics.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/routers/system.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/sse.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/api/utilities.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/database.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/gunicorn.conf.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/logging.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/models.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/py.typed +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/repository.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/scheduler.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/schemas.py +0 -0
- {servicekit-2.0.0 → servicekit-2.0.1}/src/servicekit/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: servicekit
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Async SQLAlchemy framework with FastAPI integration - reusable foundation for building data services
|
|
5
5
|
Keywords: fastapi,sqlalchemy,async,database,rest-api,crud,framework
|
|
6
6
|
Author: Morten Hansen
|
|
@@ -179,9 +179,10 @@ router = CrudRouter.create(
|
|
|
179
179
|
)
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
`POST` creates only and returns `409 Conflict` for an ID that already exists
|
|
183
|
-
omitted field keeps its value, an explicit `null` clears a nullable
|
|
184
|
-
and pagination is opt-in through `page` (>= 1) and `size` (1-100),
|
|
182
|
+
`POST` creates only and returns `409 Conflict` for an ID that already exists or for any other database constraint
|
|
183
|
+
violation. `PUT` applies the fields sent: an omitted field keeps its value, an explicit `null` clears a nullable
|
|
184
|
+
field. Collection listings are ordered by ID, and pagination is opt-in through `page` (>= 1) and `size` (1-100),
|
|
185
|
+
which return `422` when out of range.
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
## Examples
|
|
@@ -143,9 +143,10 @@ router = CrudRouter.create(
|
|
|
143
143
|
)
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
`POST` creates only and returns `409 Conflict` for an ID that already exists
|
|
147
|
-
omitted field keeps its value, an explicit `null` clears a nullable
|
|
148
|
-
and pagination is opt-in through `page` (>= 1) and `size` (1-100),
|
|
146
|
+
`POST` creates only and returns `409 Conflict` for an ID that already exists or for any other database constraint
|
|
147
|
+
violation. `PUT` applies the fields sent: an omitted field keeps its value, an explicit `null` clears a nullable
|
|
148
|
+
field. Collection listings are ordered by ID, and pagination is opt-in through `page` (>= 1) and `size` (1-100),
|
|
149
|
+
which return `422` when out of range.
|
|
149
150
|
|
|
150
151
|
|
|
151
152
|
## Examples
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "servicekit"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.1"
|
|
4
4
|
description = "Async SQLAlchemy framework with FastAPI integration - reusable foundation for building data services"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Morten Hansen", email = "morten@dhis2.org" }]
|
|
@@ -22,6 +22,7 @@ from .exceptions import (
|
|
|
22
22
|
ServicekitException,
|
|
23
23
|
UnauthorizedError,
|
|
24
24
|
ValidationError,
|
|
25
|
+
classify_integrity_error,
|
|
25
26
|
)
|
|
26
27
|
from .logging import add_request_context, clear_request_context, configure_logging, get_logger, reset_request_context
|
|
27
28
|
from .manager import BaseManager, LifecycleHooks, Manager
|
|
@@ -76,6 +77,7 @@ __all__ = [
|
|
|
76
77
|
"NotFoundError",
|
|
77
78
|
"ValidationError",
|
|
78
79
|
"ConflictError",
|
|
80
|
+
"classify_integrity_error",
|
|
79
81
|
"InvalidULIDError",
|
|
80
82
|
"BadRequestError",
|
|
81
83
|
"UnauthorizedError",
|
|
@@ -8,7 +8,7 @@ from .middleware import add_error_handlers, add_logging_middleware, database_err
|
|
|
8
8
|
from .pagination import PaginationParams, create_paginated_response
|
|
9
9
|
from .router import Router
|
|
10
10
|
from .routers import HealthRouter, HealthState, HealthStatus, JobRouter, MetricsRouter, SystemInfo, SystemRouter
|
|
11
|
-
from .service_builder import BaseServiceBuilder, ServiceInfo
|
|
11
|
+
from .service_builder import BaseServiceBuilder, JobOptions, ServiceInfo
|
|
12
12
|
from .sse import SSE_HEADERS, format_sse_event, format_sse_model_event
|
|
13
13
|
from .utilities import build_location_url, run_app
|
|
14
14
|
|
|
@@ -19,6 +19,7 @@ __all__ = [
|
|
|
19
19
|
"CrudPermissions",
|
|
20
20
|
# Service builder
|
|
21
21
|
"BaseServiceBuilder",
|
|
22
|
+
"JobOptions",
|
|
22
23
|
"ServiceInfo",
|
|
23
24
|
# App system
|
|
24
25
|
"App",
|
|
@@ -9,7 +9,22 @@ from ulid import ULID
|
|
|
9
9
|
|
|
10
10
|
from servicekit.api.router import Router
|
|
11
11
|
from servicekit.manager import Manager
|
|
12
|
-
from servicekit.schemas import PaginatedResponse
|
|
12
|
+
from servicekit.schemas import PaginatedResponse, ProblemDetail
|
|
13
|
+
|
|
14
|
+
# OpenAPI response documentation shared by CRUD routes
|
|
15
|
+
_CONFLICT_RESPONSES: dict[int | str, dict[str, Any]] = {
|
|
16
|
+
status.HTTP_409_CONFLICT: {
|
|
17
|
+
"model": ProblemDetail,
|
|
18
|
+
"description": "An entity with the supplied id already exists or a database constraint was violated",
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_NOT_FOUND_RESPONSES: dict[int | str, dict[str, Any]] = {
|
|
23
|
+
status.HTTP_404_NOT_FOUND: {
|
|
24
|
+
"model": ProblemDetail,
|
|
25
|
+
"description": "No entity exists with the supplied id",
|
|
26
|
+
}
|
|
27
|
+
}
|
|
13
28
|
|
|
14
29
|
# Type alias for manager factory function
|
|
15
30
|
type ManagerFactory[InSchemaT: BaseModel, OutSchemaT: BaseModel] = Callable[..., Manager[InSchemaT, OutSchemaT, ULID]]
|
|
@@ -171,7 +186,12 @@ class CrudRouter[InSchemaT: BaseModel, OutSchemaT: BaseModel](Router):
|
|
|
171
186
|
entity_out_annotation: Any = self.entity_out_type
|
|
172
187
|
router_prefix = self.router.prefix
|
|
173
188
|
|
|
174
|
-
@self.router.post(
|
|
189
|
+
@self.router.post(
|
|
190
|
+
"",
|
|
191
|
+
status_code=status.HTTP_201_CREATED,
|
|
192
|
+
response_model=entity_out_annotation,
|
|
193
|
+
responses=_CONFLICT_RESPONSES,
|
|
194
|
+
)
|
|
175
195
|
async def create(
|
|
176
196
|
entity_in: InSchemaT,
|
|
177
197
|
request: Request,
|
|
@@ -218,7 +238,7 @@ class CrudRouter[InSchemaT: BaseModel, OutSchemaT: BaseModel](Router):
|
|
|
218
238
|
entity_out_annotation: Any = self.entity_out_type
|
|
219
239
|
router_prefix = self.router.prefix
|
|
220
240
|
|
|
221
|
-
@self.router.get("/{entity_id}", response_model=entity_out_annotation)
|
|
241
|
+
@self.router.get("/{entity_id}", response_model=entity_out_annotation, responses=_NOT_FOUND_RESPONSES)
|
|
222
242
|
async def find_by_id(
|
|
223
243
|
entity_id: str,
|
|
224
244
|
manager: Manager[InSchemaT, OutSchemaT, ULID] = manager_dependency,
|
|
@@ -242,7 +262,7 @@ class CrudRouter[InSchemaT: BaseModel, OutSchemaT: BaseModel](Router):
|
|
|
242
262
|
entity_out_annotation: Any = self.entity_out_type
|
|
243
263
|
router_prefix = self.router.prefix
|
|
244
264
|
|
|
245
|
-
@self.router.put("/{entity_id}", response_model=entity_out_annotation)
|
|
265
|
+
@self.router.put("/{entity_id}", response_model=entity_out_annotation, responses=_NOT_FOUND_RESPONSES)
|
|
246
266
|
async def update(
|
|
247
267
|
entity_id: str,
|
|
248
268
|
entity_in: InSchemaT,
|
|
@@ -266,7 +286,7 @@ class CrudRouter[InSchemaT: BaseModel, OutSchemaT: BaseModel](Router):
|
|
|
266
286
|
def _register_delete_route(self, manager_dependency: Any, manager_annotation: Any) -> None:
|
|
267
287
|
router_prefix = self.router.prefix
|
|
268
288
|
|
|
269
|
-
@self.router.delete("/{entity_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
289
|
+
@self.router.delete("/{entity_id}", status_code=status.HTTP_204_NO_CONTENT, responses=_NOT_FOUND_RESPONSES)
|
|
270
290
|
async def delete_by_id(
|
|
271
291
|
entity_id: str,
|
|
272
292
|
manager: Manager[InSchemaT, OutSchemaT, ULID] = manager_dependency,
|
|
@@ -10,7 +10,7 @@ from pydantic import ValidationError as PydanticValidationError
|
|
|
10
10
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
11
11
|
from ulid import ULID
|
|
12
12
|
|
|
13
|
-
from servicekit.exceptions import ErrorType, ServicekitException
|
|
13
|
+
from servicekit.exceptions import ErrorType, ServicekitException, classify_integrity_error
|
|
14
14
|
from servicekit.logging import add_request_context, get_logger, reset_request_context
|
|
15
15
|
from servicekit.schemas import ProblemDetail
|
|
16
16
|
|
|
@@ -112,6 +112,8 @@ async def database_error_handler(request: Request, exc: Exception) -> JSONRespon
|
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
if isinstance(exc, IntegrityError):
|
|
115
|
+
constraint = classify_integrity_error(exc)
|
|
116
|
+
constraint_extension: dict[str, Any] = {"constraint": constraint} if constraint is not None else {}
|
|
115
117
|
problem = ProblemDetail(
|
|
116
118
|
type=ErrorType.CONFLICT,
|
|
117
119
|
title="Conflict",
|
|
@@ -119,6 +121,7 @@ async def database_error_handler(request: Request, exc: Exception) -> JSONRespon
|
|
|
119
121
|
detail="The request conflicts with existing data",
|
|
120
122
|
instance=str(request.url),
|
|
121
123
|
trace_id=trace_id,
|
|
124
|
+
**constraint_extension,
|
|
122
125
|
)
|
|
123
126
|
else:
|
|
124
127
|
problem = ProblemDetail(
|
|
@@ -7,12 +7,13 @@ from collections.abc import AsyncGenerator, Callable
|
|
|
7
7
|
from typing import Annotated, Any
|
|
8
8
|
|
|
9
9
|
import ulid
|
|
10
|
-
from fastapi import Depends,
|
|
10
|
+
from fastapi import Depends, Query, status
|
|
11
11
|
from fastapi.responses import Response, StreamingResponse
|
|
12
12
|
from pydantic import TypeAdapter
|
|
13
13
|
|
|
14
14
|
from servicekit.api.router import Router
|
|
15
15
|
from servicekit.api.sse import SSE_HEADERS, format_sse_model_event
|
|
16
|
+
from servicekit.exceptions import InvalidULIDError, NotFoundError
|
|
16
17
|
from servicekit.scheduler import Scheduler
|
|
17
18
|
from servicekit.schemas import JobRecord, JobStatus
|
|
18
19
|
|
|
@@ -57,23 +58,23 @@ class JobRouter(Router):
|
|
|
57
58
|
job_id: str,
|
|
58
59
|
scheduler: Scheduler = scheduler_dependency,
|
|
59
60
|
) -> JobRecord:
|
|
61
|
+
ulid_id = self._parse_job_id(job_id)
|
|
60
62
|
try:
|
|
61
|
-
ulid_id = ULID.from_str(job_id)
|
|
62
63
|
return await scheduler.get_record(ulid_id)
|
|
63
|
-
except
|
|
64
|
-
raise
|
|
64
|
+
except KeyError as error:
|
|
65
|
+
raise self._job_not_found(job_id) from error
|
|
65
66
|
|
|
66
67
|
@self.router.delete("/{job_id}", summary="Cancel and delete job", status_code=status.HTTP_204_NO_CONTENT)
|
|
67
68
|
async def delete_job(
|
|
68
69
|
job_id: str,
|
|
69
70
|
scheduler: Scheduler = scheduler_dependency,
|
|
70
71
|
) -> Response:
|
|
72
|
+
ulid_id = self._parse_job_id(job_id)
|
|
71
73
|
try:
|
|
72
|
-
ulid_id = ULID.from_str(job_id)
|
|
73
74
|
await scheduler.delete(ulid_id)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
except KeyError as error:
|
|
76
|
+
raise self._job_not_found(job_id) from error
|
|
77
|
+
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
77
78
|
|
|
78
79
|
@self.router.get(
|
|
79
80
|
"/{job_id}/$stream",
|
|
@@ -87,16 +88,13 @@ class JobRouter(Router):
|
|
|
87
88
|
) -> StreamingResponse:
|
|
88
89
|
"""Stream real-time job status updates using Server-Sent Events."""
|
|
89
90
|
# Validate job_id format
|
|
90
|
-
|
|
91
|
-
ulid_id = ULID.from_str(job_id)
|
|
92
|
-
except ValueError:
|
|
93
|
-
raise HTTPException(status_code=400, detail="Invalid job ID format")
|
|
91
|
+
ulid_id = self._parse_job_id(job_id)
|
|
94
92
|
|
|
95
93
|
# Check job exists before starting stream
|
|
96
94
|
try:
|
|
97
95
|
await scheduler.get_record(ulid_id)
|
|
98
|
-
except KeyError:
|
|
99
|
-
raise
|
|
96
|
+
except KeyError as error:
|
|
97
|
+
raise self._job_not_found(job_id) from error
|
|
100
98
|
|
|
101
99
|
# SSE event generator
|
|
102
100
|
async def event_stream() -> AsyncGenerator[bytes, None]:
|
|
@@ -124,3 +122,17 @@ class JobRouter(Router):
|
|
|
124
122
|
media_type="text/event-stream",
|
|
125
123
|
headers=SSE_HEADERS,
|
|
126
124
|
)
|
|
125
|
+
|
|
126
|
+
def _parse_job_id(self, job_id: str) -> ULID:
|
|
127
|
+
"""Parse a job ID as a ULID, raising InvalidULIDError when malformed."""
|
|
128
|
+
try:
|
|
129
|
+
return ULID.from_str(job_id)
|
|
130
|
+
except ValueError as error:
|
|
131
|
+
raise InvalidULIDError(
|
|
132
|
+
f"Invalid job ID format: {job_id}",
|
|
133
|
+
instance=f"{self.router.prefix}/{job_id}",
|
|
134
|
+
) from error
|
|
135
|
+
|
|
136
|
+
def _job_not_found(self, job_id: str) -> NotFoundError:
|
|
137
|
+
"""Build the not-found error for a missing job."""
|
|
138
|
+
return NotFoundError(f"Job with id {job_id} not found", instance=f"{self.router.prefix}/{job_id}")
|
|
@@ -54,7 +54,7 @@ class _SystemOptions:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
@dataclass(frozen=True)
|
|
57
|
-
class
|
|
57
|
+
class JobOptions:
|
|
58
58
|
"""Configuration for job scheduler endpoints."""
|
|
59
59
|
|
|
60
60
|
prefix: str
|
|
@@ -63,6 +63,10 @@ class _JobOptions:
|
|
|
63
63
|
shutdown_timeout: float
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
# Backwards-compatible alias for the previously private name
|
|
67
|
+
_JobOptions = JobOptions
|
|
68
|
+
|
|
69
|
+
|
|
66
70
|
@dataclass(frozen=True)
|
|
67
71
|
class _AuthOptions:
|
|
68
72
|
"""Configuration for API key authentication."""
|
|
@@ -153,7 +157,7 @@ class BaseServiceBuilder:
|
|
|
153
157
|
self._include_logging = include_logging
|
|
154
158
|
self._health_options: _HealthOptions | None = None
|
|
155
159
|
self._system_options: _SystemOptions | None = None
|
|
156
|
-
self._job_options:
|
|
160
|
+
self._job_options: JobOptions | None = None
|
|
157
161
|
self._auth_options: _AuthOptions | None = None
|
|
158
162
|
self._monitoring_options: _MonitoringOptions | None = None
|
|
159
163
|
self._registration_options: _RegistrationOptions | None = None
|
|
@@ -247,7 +251,7 @@ class BaseServiceBuilder:
|
|
|
247
251
|
shutdown_timeout: float = 10.0,
|
|
248
252
|
) -> Self:
|
|
249
253
|
"""Add job scheduler endpoints."""
|
|
250
|
-
self._job_options =
|
|
254
|
+
self._job_options = JobOptions(
|
|
251
255
|
prefix=prefix,
|
|
252
256
|
tags=list(tags) if tags is not None else ["Jobs"],
|
|
253
257
|
max_concurrency=max_concurrency,
|
|
@@ -776,7 +780,7 @@ class BaseServiceBuilder:
|
|
|
776
780
|
|
|
777
781
|
return lifespan
|
|
778
782
|
|
|
779
|
-
def _create_scheduler(self, job_options:
|
|
783
|
+
def _create_scheduler(self, job_options: JobOptions) -> Scheduler:
|
|
780
784
|
"""Create the scheduler used by the application (override in subclasses)."""
|
|
781
785
|
from servicekit.scheduler import InMemoryScheduler
|
|
782
786
|
|
|
@@ -137,3 +137,18 @@ class ForbiddenError(ServicekitException):
|
|
|
137
137
|
instance=instance,
|
|
138
138
|
**extensions,
|
|
139
139
|
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def classify_integrity_error(error: Exception) -> str | None:
|
|
143
|
+
"""Classify a database integrity error as a safe constraint name, or None when unrecognized."""
|
|
144
|
+
driver_error = getattr(error, "orig", None)
|
|
145
|
+
message = str(driver_error if driver_error is not None else error).upper()
|
|
146
|
+
if "FOREIGN KEY" in message:
|
|
147
|
+
return "foreign_key"
|
|
148
|
+
if "UNIQUE" in message:
|
|
149
|
+
return "unique"
|
|
150
|
+
if "NOT NULL" in message:
|
|
151
|
+
return "not_null"
|
|
152
|
+
if "CHECK" in message:
|
|
153
|
+
return "check"
|
|
154
|
+
return None
|
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Iterable, Sequence
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
from sqlalchemy.exc import IntegrityError
|
|
10
10
|
|
|
11
|
-
from servicekit.exceptions import ConflictError
|
|
11
|
+
from servicekit.exceptions import ConflictError, classify_integrity_error
|
|
12
12
|
from servicekit.repository import BaseRepository
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -154,7 +154,11 @@ class BaseManager[ModelT, InSchemaT: BaseModel, OutSchemaT: BaseModel, IdT](
|
|
|
154
154
|
await self.repo.commit()
|
|
155
155
|
except IntegrityError as e:
|
|
156
156
|
await self.repo.rollback()
|
|
157
|
-
|
|
157
|
+
if entity_id is not None and await self.repo.exists_by_id(entity_id):
|
|
158
|
+
raise ConflictError(f"Entity with id {entity_id} already exists") from e
|
|
159
|
+
constraint = classify_integrity_error(e)
|
|
160
|
+
extensions = {"constraint": constraint} if constraint is not None else {}
|
|
161
|
+
raise ConflictError("Entity violates a database constraint", **extensions) from e
|
|
158
162
|
await self.repo.refresh_many([entity])
|
|
159
163
|
await self.post_save(entity)
|
|
160
164
|
return self._to_output_schema(entity)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|