python-ddd-framework 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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 (104) hide show
  1. python_ddd_framework/application/runtime.py +46 -19
  2. python_ddd_framework/application_services/bindings.py +0 -64
  3. python_ddd_framework/application_services/dispatcher.py +1 -1
  4. python_ddd_framework/application_services/invocation.py +7 -500
  5. python_ddd_framework/application_services/module.py +16 -5
  6. python_ddd_framework/authorization/module.py +1 -1
  7. python_ddd_framework/background_execution/local.py +20 -1
  8. python_ddd_framework/background_execution/processes.py +11 -1
  9. python_ddd_framework/background_jobs/execution.py +1 -1
  10. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +1 -1
  11. python_ddd_framework/background_workers/execution.py +1 -1
  12. python_ddd_framework/background_workers/runtime.py +11 -2
  13. python_ddd_framework/caching/unit_of_work.py +2 -4
  14. python_ddd_framework/cli/project.py +11 -10
  15. python_ddd_framework/developer_kit/generation.py +46 -36
  16. python_ddd_framework/developer_kit/publication.py +442 -0
  17. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +42 -3
  18. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +31 -7
  19. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +2 -2
  20. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +3 -1
  21. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +21 -3
  22. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +18 -1
  23. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +74 -3
  24. python_ddd_framework/diagnostics/source.py +14 -1
  25. python_ddd_framework/domain/aggregates.py +5 -12
  26. python_ddd_framework/domain/event_values.py +124 -0
  27. python_ddd_framework/events/runtime.py +2 -7
  28. python_ddd_framework/events/unit_of_work.py +1 -4
  29. python_ddd_framework/fastapi/action.py +2 -1
  30. python_ddd_framework/fastapi/adapter.py +11 -117
  31. python_ddd_framework/fastapi/application_services.py +17 -402
  32. python_ddd_framework/fastapi/error_handlers.py +128 -0
  33. python_ddd_framework/fastapi/realtime/runtime.py +1 -1
  34. python_ddd_framework/fastapi/request_context.py +2 -5
  35. python_ddd_framework/fastapi/routing.py +4 -10
  36. python_ddd_framework/fastapi/service_endpoints.py +182 -0
  37. python_ddd_framework/fastapi/service_routes.py +237 -0
  38. python_ddd_framework/fastapi/transfer.py +1 -1
  39. python_ddd_framework/hosted_services/bridge.py +76 -21
  40. python_ddd_framework/hosted_services/contracts.py +5 -1
  41. python_ddd_framework/hosted_services/runtime.py +71 -17
  42. python_ddd_framework/identity/__init__.py +21 -13
  43. python_ddd_framework/identity/application.py +12 -66
  44. python_ddd_framework/identity/contracts.py +38 -411
  45. python_ddd_framework/identity/domain_rules.py +17 -0
  46. python_ddd_framework/identity/dtos.py +152 -0
  47. python_ddd_framework/identity/extensions.py +9 -0
  48. python_ddd_framework/identity/management.py +28 -25
  49. python_ddd_framework/identity/models.py +86 -0
  50. python_ddd_framework/identity/module.py +3 -4
  51. python_ddd_framework/identity/options.py +54 -0
  52. python_ddd_framework/identity/seeding.py +59 -0
  53. python_ddd_framework/identity/services.py +1 -1
  54. python_ddd_framework/identity/sqlalchemy/module.py +9 -6
  55. python_ddd_framework/identity/sqlalchemy/repository.py +4 -5
  56. python_ddd_framework/identity/sqlalchemy/session_security.py +1 -3
  57. python_ddd_framework/identity/sqlalchemy/stores.py +35 -47
  58. python_ddd_framework/identity/stores.py +120 -0
  59. python_ddd_framework/identity/tokens.py +1 -1
  60. python_ddd_framework/invocation/callables.py +1 -1
  61. python_ddd_framework/invocation/dispatcher.py +1 -1
  62. python_ddd_framework/invocation/entrypoints.py +2 -1
  63. python_ddd_framework/invocation/function_runtime.py +2 -2
  64. python_ddd_framework/invocation/interception.py +1 -1
  65. python_ddd_framework/invocation/managed_proxy.py +2 -1
  66. python_ddd_framework/invocation/managed_services.py +8 -14
  67. python_ddd_framework/invocation/methods.py +2 -7
  68. python_ddd_framework/invocation/module.py +1 -1
  69. python_ddd_framework/invocation/runtime.py +483 -0
  70. python_ddd_framework/invocation/scopes.py +91 -0
  71. python_ddd_framework/lifecycle/participants.py +4 -0
  72. python_ddd_framework/messaging/channel.py +35 -8
  73. python_ddd_framework/messaging/contracts.py +9 -0
  74. python_ddd_framework/messaging/module.py +13 -0
  75. python_ddd_framework/messaging/runtime.py +17 -2
  76. python_ddd_framework/modularity/discovery.py +2 -7
  77. python_ddd_framework/modularity/graph.py +2 -8
  78. python_ddd_framework/observability/logging.py +1 -1
  79. python_ddd_framework/observability/tracing.py +1 -1
  80. python_ddd_framework/redis/cache.py +2 -0
  81. python_ddd_framework/redis/notification_runtime.py +1 -1
  82. python_ddd_framework/services/arbitration.py +1 -20
  83. python_ddd_framework/services/composition.py +71 -2
  84. python_ddd_framework/services/provider.py +23 -1
  85. python_ddd_framework/services/relocation.py +96 -0
  86. python_ddd_framework/settings/refresh.py +1 -1
  87. python_ddd_framework/settings/sqlalchemy/store.py +1 -1
  88. python_ddd_framework/sqlalchemy/metadata.py +11 -413
  89. python_ddd_framework/sqlalchemy/metadata_builder.py +234 -0
  90. python_ddd_framework/sqlalchemy/metadata_fingerprint.py +108 -0
  91. python_ddd_framework/sqlalchemy/metadata_ownership.py +73 -0
  92. python_ddd_framework/sqlalchemy/migration.py +2 -1
  93. python_ddd_framework/sqlalchemy/migration_sources.py +38 -0
  94. python_ddd_framework/sqlalchemy/module.py +2 -1
  95. python_ddd_framework/sqlalchemy/unit_of_work.py +1 -1
  96. python_ddd_framework/unit_of_work/contracts.py +68 -7
  97. python_ddd_framework/unit_of_work/manager.py +4 -7
  98. python_ddd_framework/unit_of_work/module.py +1 -1
  99. {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/METADATA +82 -4
  100. {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/RECORD +104 -85
  101. {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/WHEEL +1 -1
  102. {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/entry_points.txt +0 -0
  103. {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/licenses/LICENSE +0 -0
  104. {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
@@ -1,448 +1,75 @@
1
- """Identity 的 HTTP/Application contracts、持久化值与 Host keyring Options。"""
1
+ """Identity 应用服务契约与所属权限名称。"""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Mapping
6
- from dataclasses import dataclass, field, replace
7
- from datetime import datetime, timedelta
8
- from enum import Enum
9
- from typing import Annotated, Protocol
5
+ from typing import Protocol
10
6
  from uuid import UUID
11
7
 
12
- from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
13
-
14
8
  from ..application_services import ApplicationServiceContract, PagedResult, PagedResultRequestDto
15
- from ..authorization import (
16
- CurrentUser,
17
- PermissionName,
18
- RolePermissionVersion,
19
- )
20
- from ..domain import OptimisticConcurrencyError
21
- from ..extensions.contracts import ExtensibleModel, ExtensionProperties, ExtensionTarget
22
- from ..options import BaseOptions
9
+ from ..authorization import PermissionName
10
+ from . import dtos
23
11
 
24
12
  IDENTITY_USERS_MANAGE = PermissionName("identity.users.manage")
25
- IDENTITY_ROLES_MANAGE = PermissionName("identity.roles.manage")
26
-
27
-
28
- class IdentityOptions(BaseOptions):
29
- issuer: str
30
- audience: str
31
- active_kid: str
32
- keys: Mapping[str, SecretStr]
33
- access_token_lifetime: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
34
- refresh_inactivity_lifetime: timedelta = Field(default=timedelta(days=30), gt=timedelta(0))
35
- refresh_absolute_lifetime: timedelta = Field(default=timedelta(days=90), gt=timedelta(0))
36
- failed_attempt_limit: int = Field(default=5, ge=1)
37
- lockout_duration: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
38
- seed_admin_username: str | None = None
39
- seed_admin_password: SecretStr | None = None
40
-
41
- @field_validator("issuer", "audience", "active_kid")
42
- @classmethod
43
- def _validate_identifier(cls, value: str) -> str:
44
- if not value or value != value.strip():
45
- raise ValueError("JWT identifier must be a non-empty trimmed string")
46
- return value
47
-
48
- @field_validator("keys")
49
- @classmethod
50
- def _validate_keys(cls, value: Mapping[str, SecretStr]) -> Mapping[str, SecretStr]:
51
- if not value:
52
- raise ValueError("JWT keyring must contain at least one key")
53
- for kid, secret in value.items():
54
- if not kid or kid != kid.strip():
55
- raise ValueError("JWT kid must be a non-empty trimmed string")
56
- if len(secret.get_secret_value().encode("utf-8")) < 32:
57
- raise ValueError("HS256 keys must contain at least 256 bits")
58
- return value
59
-
60
- @model_validator(mode="after")
61
- def _validate_active_and_seed(self) -> IdentityOptions:
62
- if self.active_kid not in self.keys:
63
- raise ValueError("active_kid must reference the JWT keyring")
64
- if (self.seed_admin_username is None) != (self.seed_admin_password is None):
65
- raise ValueError("seed administrator username and password must be configured together")
66
- if self.seed_admin_username is not None:
67
- _normalize_name(self.seed_admin_username, role="seed administrator username")
68
- return self
69
-
70
-
71
- class LoginCommand(BaseModel):
72
- model_config = ConfigDict(frozen=True)
73
-
74
- username: Annotated[str, Field(min_length=1, max_length=120)]
75
- password: SecretStr
76
-
77
-
78
- class RefreshTokenCommand(BaseModel):
79
- model_config = ConfigDict(frozen=True)
80
-
81
- refresh_token: SecretStr
82
-
83
-
84
- class RevokeTokenCommand(BaseModel):
85
- model_config = ConfigDict(frozen=True)
86
-
87
- refresh_token: SecretStr
88
-
89
-
90
- class CreateUserCommand(ExtensibleModel):
91
- model_config = ConfigDict(frozen=True)
92
-
93
- username: Annotated[str, Field(min_length=1, max_length=120)]
94
- password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
95
-
96
- @field_validator("username")
97
- @classmethod
98
- def _validate_username(cls, value: str) -> str:
99
- _normalize_name(value, role="username")
100
- return value.strip()
101
-
102
-
103
- class CreateRoleCommand(BaseModel):
104
- model_config = ConfigDict(frozen=True)
105
-
106
- name: Annotated[str, Field(min_length=1, max_length=120)]
107
-
108
- @field_validator("name")
109
- @classmethod
110
- def _validate_name(cls, value: str) -> str:
111
- _normalize_name(value, role="role name")
112
- return value.strip()
113
-
114
-
115
- class IdentityConcurrencyCommand(BaseModel):
116
- model_config = ConfigDict(frozen=True)
117
-
118
- concurrency_version: Annotated[int, Field(ge=0)]
119
-
120
-
121
- class UpdateUserCommand(ExtensibleModel):
122
- model_config = ConfigDict(frozen=True)
123
-
124
- username: Annotated[str, Field(min_length=1, max_length=120)]
125
- concurrency_version: Annotated[int, Field(ge=0)]
126
-
127
- @field_validator("username")
128
- @classmethod
129
- def _validate_username(cls, value: str) -> str:
130
- _normalize_name(value, role="username")
131
- return value.strip()
132
-
133
-
134
- class SetUserActiveCommand(IdentityConcurrencyCommand):
135
- is_active: bool
136
-
137
-
138
- class UpdateRoleCommand(IdentityConcurrencyCommand):
139
- name: Annotated[str, Field(min_length=1, max_length=120)]
140
-
141
- @field_validator("name")
142
- @classmethod
143
- def _validate_name(cls, value: str) -> str:
144
- _normalize_name(value, role="role name")
145
- return value.strip()
146
-
147
-
148
- class ChangePasswordCommand(IdentityConcurrencyCommand):
149
- old_password: SecretStr
150
- new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
151
-
152
-
153
- class SetPasswordCommand(IdentityConcurrencyCommand):
154
- new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
155
-
156
-
157
- class SetUserRolesCommand(IdentityConcurrencyCommand):
158
- model_config = ConfigDict(frozen=True)
159
-
160
- role_ids: tuple[UUID, ...]
161
-
162
-
163
- class SetRolePermissionsCommand(IdentityConcurrencyCommand):
164
- model_config = ConfigDict(frozen=True)
165
-
166
- permissions: tuple[str, ...]
167
-
168
-
169
- @dataclass(frozen=True, slots=True)
170
- class TokenPair:
171
- access_token: str
172
- refresh_token: str
173
- token_type: str
174
- expires_in: int
175
-
176
-
177
- class UserView(ExtensibleModel):
178
- user_id: UUID
179
- username: str
180
- is_active: bool
181
- role_ids: tuple[UUID, ...]
182
- concurrency_version: int
183
-
184
-
185
- @dataclass(frozen=True, slots=True)
186
- class RoleView:
187
- role_id: UUID
188
- name: str
189
- permissions: tuple[str, ...]
190
- concurrency_version: int
191
-
192
-
193
- class UserRolesView(BaseModel):
194
- role_ids: tuple[UUID, ...]
195
- concurrency_version: int
196
-
197
13
 
198
- class RolePermissionsView(BaseModel):
199
- permissions: tuple[str, ...]
200
- concurrency_version: int
201
14
 
202
-
203
- @dataclass(frozen=True, slots=True)
204
- class CurrentUserView:
205
- user_id: UUID
206
- session_id: UUID
207
- username: str
208
- concurrency_version: int
209
-
210
-
211
- @dataclass(frozen=True, slots=True)
212
- class IdentityUser:
213
- user_id: UUID
214
- username: str
215
- normalized_username: str
216
- password_hash: str
217
- is_active: bool
218
- failed_access_count: int
219
- lockout_end: datetime | None
220
- permission_version: int
221
- extra_properties: ExtensionProperties = field(default_factory=ExtensionProperties)
222
- concurrency_version: int = 0
223
-
224
- def renamed(self, username: str, expected_version: int) -> IdentityUser:
225
- _check_version(self.concurrency_version, expected_version)
226
- return replace(
227
- self,
228
- username=username.strip(),
229
- normalized_username=_normalize_name(username, role="username"),
230
- concurrency_version=self.concurrency_version + 1,
231
- )
232
-
233
- def with_active(self, is_active: bool, expected_version: int) -> IdentityUser:
234
- _check_version(self.concurrency_version, expected_version)
235
- return replace(
236
- self,
237
- is_active=is_active,
238
- concurrency_version=self.concurrency_version + (self.is_active != is_active),
239
- )
240
-
241
-
242
- IDENTITY_USER_EXTENSION = ExtensionTarget(
243
- "identity.user", (CreateUserCommand, UpdateUserCommand, UserView), IdentityUser
244
- )
245
-
246
-
247
- @dataclass(frozen=True, slots=True)
248
- class IdentityRole:
249
- role_id: UUID
250
- name: str
251
- normalized_name: str
252
- permission_version: int
253
- concurrency_version: int = 0
254
-
255
- def renamed(self, name: str, expected_version: int) -> IdentityRole:
256
- _check_version(self.concurrency_version, expected_version)
257
- return replace(
258
- self,
259
- name=name.strip(),
260
- normalized_name=_normalize_name(name, role="role name"),
261
- concurrency_version=self.concurrency_version + 1,
262
- )
263
-
264
-
265
- @dataclass(frozen=True, slots=True)
266
- class RefreshSession:
267
- session_id: UUID
268
- user_id: UUID
269
- family_id: UUID
270
- token_hash: str
271
- created_at: datetime
272
- family_created_at: datetime
273
- last_used_at: datetime
274
- revoked_at: datetime | None = None
275
- replaced_by_session_id: UUID | None = None
276
-
277
-
278
- class RefreshRotationStatus(Enum):
279
- ROTATED = "rotated"
280
- NOT_FOUND = "not_found"
281
- REUSED = "reused"
282
- EXPIRED = "expired"
283
-
284
-
285
- @dataclass(frozen=True, slots=True)
286
- class RefreshRotationResult:
287
- status: RefreshRotationStatus
288
- session: RefreshSession | None
289
-
290
-
291
- class IdentityRepository(Protocol):
292
- async def find_user_by_normalized_username(
293
- self, normalized_username: str
294
- ) -> IdentityUser | None: ...
295
-
296
- async def create_user(self, user: IdentityUser) -> None: ...
297
-
298
- async def get_user(self, user_id: UUID) -> IdentityUser: ...
299
-
300
- async def get_user_view(self, user_id: UUID) -> UserView: ...
301
-
302
- async def list_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
303
-
304
- async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView: ...
305
-
306
- async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView: ...
307
-
308
- async def delete_user(self, user_id: UUID, concurrency_version: int) -> None: ...
309
-
310
- async def set_password(
311
- self,
312
- user_id: UUID,
313
- password_hash: str,
314
- concurrency_version: int,
315
- *,
316
- expected_password_hash: str | None = None,
317
- ) -> None: ...
318
-
319
- async def create_role(self, role: IdentityRole) -> None: ...
320
-
321
- async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None: ...
322
-
323
- async def get_role(self, role_id: UUID) -> RoleView: ...
324
-
325
- async def list_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
326
-
327
- async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView: ...
328
-
329
- async def delete_role(self, role_id: UUID, concurrency_version: int) -> None: ...
330
-
331
- async def set_user_roles(
332
- self, user_id: UUID, role_ids: tuple[UUID, ...], concurrency_version: int
333
- ) -> UserRolesView: ...
334
-
335
- async def set_role_permissions(
336
- self, role_id: UUID, permissions: tuple[str, ...], concurrency_version: int
337
- ) -> RolePermissionsView: ...
338
-
339
-
340
- class IdentityAccessStore(Protocol):
341
- async def load_current_user(
342
- self,
343
- user_id: UUID,
344
- session_id: UUID,
345
- now: datetime,
346
- ) -> CurrentUser | None: ...
347
-
348
- async def permissions_for_roles(
349
- self, role_versions: tuple[RolePermissionVersion, ...]
350
- ) -> frozenset[str]: ...
351
-
352
-
353
- class IdentityLoginStore(Protocol):
354
- async def find_user(self, normalized_username: str) -> IdentityUser | None: ...
355
-
356
- async def record_failure(
357
- self,
358
- user_id: UUID,
359
- *,
360
- now: datetime,
361
- attempt_limit: int,
362
- lockout_duration_seconds: int,
363
- ) -> None: ...
364
-
365
- async def complete_success(
366
- self,
367
- user_id: UUID,
368
- *,
369
- expected_password_hash: str,
370
- updated_password_hash: str | None,
371
- session: RefreshSession,
372
- now: datetime,
373
- ) -> bool: ...
374
-
375
-
376
- class RefreshSessionStore(Protocol):
377
- async def rotate(
378
- self,
379
- token_hash: str,
380
- *,
381
- replacement_id: UUID,
382
- replacement_hash: str,
383
- now: datetime,
384
- ) -> RefreshRotationResult: ...
385
-
386
- async def revoke(self, token_hash: str, *, now: datetime) -> None: ...
15
+ IDENTITY_ROLES_MANAGE = PermissionName("identity.roles.manage")
387
16
 
388
17
 
389
18
  class AuthenticationApplicationService(ApplicationServiceContract, Protocol):
390
- async def login(self, command: LoginCommand) -> TokenPair: ...
19
+ async def login(self, command: dtos.LoginCommand) -> dtos.TokenPair: ...
391
20
 
392
- async def refresh(self, command: RefreshTokenCommand) -> TokenPair: ...
21
+ async def refresh(self, command: dtos.RefreshTokenCommand) -> dtos.TokenPair: ...
393
22
 
394
- async def revoke(self, command: RevokeTokenCommand) -> None: ...
23
+ async def revoke(self, command: dtos.RevokeTokenCommand) -> None: ...
395
24
 
396
- async def me(self) -> CurrentUserView: ...
25
+ async def me(self) -> dtos.CurrentUserView: ...
397
26
 
398
- async def change_password(self, command: ChangePasswordCommand) -> None: ...
27
+ async def change_password(self, command: dtos.ChangePasswordCommand) -> None: ...
399
28
 
400
29
 
401
30
  class IdentityManagementApplicationService(ApplicationServiceContract, Protocol):
402
- async def create_user(self, command: CreateUserCommand) -> UserView: ...
31
+ async def create_user(self, command: dtos.CreateUserCommand) -> dtos.UserView: ...
403
32
 
404
- async def get_user(self, user_id: UUID) -> UserView: ...
33
+ async def get_user(self, user_id: UUID) -> dtos.UserView: ...
405
34
 
406
- async def get_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
35
+ async def get_users(self, query: PagedResultRequestDto) -> PagedResult[dtos.UserView]: ...
407
36
 
408
- async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView: ...
37
+ async def update_user(
38
+ self, user_id: UUID, command: dtos.UpdateUserCommand
39
+ ) -> dtos.UserView: ...
409
40
 
410
- async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView: ...
41
+ async def set_user_active(
42
+ self, user_id: UUID, command: dtos.SetUserActiveCommand
43
+ ) -> dtos.UserView: ...
411
44
 
412
- async def delete_user(self, user_id: UUID, command: IdentityConcurrencyCommand) -> None: ...
45
+ async def delete_user(
46
+ self, user_id: UUID, command: dtos.IdentityConcurrencyCommand
47
+ ) -> None: ...
413
48
 
414
- async def set_password(self, user_id: UUID, command: SetPasswordCommand) -> None: ...
49
+ async def set_password(self, user_id: UUID, command: dtos.SetPasswordCommand) -> None: ...
415
50
 
416
- async def create_role(self, command: CreateRoleCommand) -> RoleView: ...
51
+ async def create_role(self, command: dtos.CreateRoleCommand) -> dtos.RoleView: ...
417
52
 
418
- async def get_role(self, role_id: UUID) -> RoleView: ...
53
+ async def get_role(self, role_id: UUID) -> dtos.RoleView: ...
419
54
 
420
- async def get_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
55
+ async def get_roles(self, query: PagedResultRequestDto) -> PagedResult[dtos.RoleView]: ...
421
56
 
422
- async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView: ...
57
+ async def update_role(
58
+ self, role_id: UUID, command: dtos.UpdateRoleCommand
59
+ ) -> dtos.RoleView: ...
423
60
 
424
- async def delete_role(self, role_id: UUID, command: IdentityConcurrencyCommand) -> None: ...
61
+ async def delete_role(
62
+ self, role_id: UUID, command: dtos.IdentityConcurrencyCommand
63
+ ) -> None: ...
425
64
 
426
- async def get_user_roles(self, user_id: UUID) -> UserRolesView: ...
65
+ async def get_user_roles(self, user_id: UUID) -> dtos.UserRolesView: ...
427
66
 
428
- async def get_role_permissions(self, role_id: UUID) -> RolePermissionsView: ...
67
+ async def get_role_permissions(self, role_id: UUID) -> dtos.RolePermissionsView: ...
429
68
 
430
69
  async def set_user_roles(
431
- self, user_id: UUID, command: SetUserRolesCommand
432
- ) -> UserRolesView: ...
70
+ self, user_id: UUID, command: dtos.SetUserRolesCommand
71
+ ) -> dtos.UserRolesView: ...
433
72
 
434
73
  async def set_role_permissions(
435
- self, role_id: UUID, command: SetRolePermissionsCommand
436
- ) -> RolePermissionsView: ...
437
-
438
-
439
- def _normalize_name(value: str, *, role: str) -> str:
440
- normalized = value.strip().casefold()
441
- if not normalized or len(normalized) > 120:
442
- raise ValueError(f"{role} must contain between 1 and 120 characters")
443
- return normalized
444
-
445
-
446
- def _check_version(actual: int, expected: int) -> None:
447
- if actual != expected:
448
- raise OptimisticConcurrencyError
74
+ self, role_id: UUID, command: dtos.SetRolePermissionsCommand
75
+ ) -> dtos.RolePermissionsView: ...
@@ -0,0 +1,17 @@
1
+ """Identity 名称规范化与领域并发版本规则。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..domain import OptimisticConcurrencyError
6
+
7
+
8
+ def _normalize_name(value: str, *, role: str) -> str:
9
+ normalized = value.strip().casefold()
10
+ if not normalized or len(normalized) > 120:
11
+ raise ValueError(f"{role} must contain between 1 and 120 characters")
12
+ return normalized
13
+
14
+
15
+ def _check_version(actual: int, expected: int) -> None:
16
+ if actual != expected:
17
+ raise OptimisticConcurrencyError
@@ -0,0 +1,152 @@
1
+ """Identity 应用服务的输入、输出与并发版本。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Annotated
7
+ from uuid import UUID
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator
10
+
11
+ from ..extensions.contracts import ExtensibleModel
12
+ from .domain_rules import _normalize_name
13
+
14
+
15
+ class LoginCommand(BaseModel):
16
+ model_config = ConfigDict(frozen=True)
17
+
18
+ username: Annotated[str, Field(min_length=1, max_length=120)]
19
+ password: SecretStr
20
+
21
+
22
+ class RefreshTokenCommand(BaseModel):
23
+ model_config = ConfigDict(frozen=True)
24
+
25
+ refresh_token: SecretStr
26
+
27
+
28
+ class RevokeTokenCommand(BaseModel):
29
+ model_config = ConfigDict(frozen=True)
30
+
31
+ refresh_token: SecretStr
32
+
33
+
34
+ class CreateUserCommand(ExtensibleModel):
35
+ model_config = ConfigDict(frozen=True)
36
+
37
+ username: Annotated[str, Field(min_length=1, max_length=120)]
38
+ password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
39
+
40
+ @field_validator("username")
41
+ @classmethod
42
+ def _validate_username(cls, value: str) -> str:
43
+ _normalize_name(value, role="username")
44
+ return value.strip()
45
+
46
+
47
+ class CreateRoleCommand(BaseModel):
48
+ model_config = ConfigDict(frozen=True)
49
+
50
+ name: Annotated[str, Field(min_length=1, max_length=120)]
51
+
52
+ @field_validator("name")
53
+ @classmethod
54
+ def _validate_name(cls, value: str) -> str:
55
+ _normalize_name(value, role="role name")
56
+ return value.strip()
57
+
58
+
59
+ class IdentityConcurrencyCommand(BaseModel):
60
+ model_config = ConfigDict(frozen=True)
61
+
62
+ concurrency_version: Annotated[int, Field(ge=0)]
63
+
64
+
65
+ class UpdateUserCommand(ExtensibleModel):
66
+ model_config = ConfigDict(frozen=True)
67
+
68
+ username: Annotated[str, Field(min_length=1, max_length=120)]
69
+ concurrency_version: Annotated[int, Field(ge=0)]
70
+
71
+ @field_validator("username")
72
+ @classmethod
73
+ def _validate_username(cls, value: str) -> str:
74
+ _normalize_name(value, role="username")
75
+ return value.strip()
76
+
77
+
78
+ class SetUserActiveCommand(IdentityConcurrencyCommand):
79
+ is_active: bool
80
+
81
+
82
+ class UpdateRoleCommand(IdentityConcurrencyCommand):
83
+ name: Annotated[str, Field(min_length=1, max_length=120)]
84
+
85
+ @field_validator("name")
86
+ @classmethod
87
+ def _validate_name(cls, value: str) -> str:
88
+ _normalize_name(value, role="role name")
89
+ return value.strip()
90
+
91
+
92
+ class ChangePasswordCommand(IdentityConcurrencyCommand):
93
+ old_password: SecretStr
94
+ new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
95
+
96
+
97
+ class SetPasswordCommand(IdentityConcurrencyCommand):
98
+ new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
99
+
100
+
101
+ class SetUserRolesCommand(IdentityConcurrencyCommand):
102
+ model_config = ConfigDict(frozen=True)
103
+
104
+ role_ids: tuple[UUID, ...]
105
+
106
+
107
+ class SetRolePermissionsCommand(IdentityConcurrencyCommand):
108
+ model_config = ConfigDict(frozen=True)
109
+
110
+ permissions: tuple[str, ...]
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class TokenPair:
115
+ access_token: str
116
+ refresh_token: str
117
+ token_type: str
118
+ expires_in: int
119
+
120
+
121
+ class UserView(ExtensibleModel):
122
+ user_id: UUID
123
+ username: str
124
+ is_active: bool
125
+ role_ids: tuple[UUID, ...]
126
+ concurrency_version: int
127
+
128
+
129
+ @dataclass(frozen=True, slots=True)
130
+ class RoleView:
131
+ role_id: UUID
132
+ name: str
133
+ permissions: tuple[str, ...]
134
+ concurrency_version: int
135
+
136
+
137
+ class UserRolesView(BaseModel):
138
+ role_ids: tuple[UUID, ...]
139
+ concurrency_version: int
140
+
141
+
142
+ class RolePermissionsView(BaseModel):
143
+ permissions: tuple[str, ...]
144
+ concurrency_version: int
145
+
146
+
147
+ @dataclass(frozen=True, slots=True)
148
+ class CurrentUserView:
149
+ user_id: UUID
150
+ session_id: UUID
151
+ username: str
152
+ concurrency_version: int
@@ -0,0 +1,9 @@
1
+ """Identity 用户扩展点连接 DTO 与领域值,由所属 Module 声明。"""
2
+
3
+ from ..extensions.contracts import ExtensionTarget
4
+ from .dtos import CreateUserCommand, UpdateUserCommand, UserView
5
+ from .models import IdentityUser
6
+
7
+ IDENTITY_USER_EXTENSION = ExtensionTarget(
8
+ "identity.user", (CreateUserCommand, UpdateUserCommand, UserView), IdentityUser
9
+ )