fastapi-modular 0.1.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 (69) hide show
  1. fastapi_modular-0.1.0.dist-info/METADATA +377 -0
  2. fastapi_modular-0.1.0.dist-info/RECORD +69 -0
  3. fastapi_modular-0.1.0.dist-info/WHEEL +4 -0
  4. fastapi_modular-0.1.0.dist-info/entry_points.txt +3 -0
  5. fastapi_modular-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. pymodular/__init__.py +74 -0
  7. pymodular/cli/__init__.py +0 -0
  8. pymodular/cli/clean.py +39 -0
  9. pymodular/cli/configure_env.py +569 -0
  10. pymodular/cli/cong_cu.py +111 -0
  11. pymodular/cli/info.py +62 -0
  12. pymodular/cli/install.py +83 -0
  13. pymodular/cli/main.py +247 -0
  14. pymodular/cli/new_module.py +492 -0
  15. pymodular/cli/new_project.py +471 -0
  16. pymodular/cli/serve.py +59 -0
  17. pymodular/core/__init__.py +0 -0
  18. pymodular/core/clock.py +15 -0
  19. pymodular/core/compat.py +39 -0
  20. pymodular/core/config.py +495 -0
  21. pymodular/core/container.py +354 -0
  22. pymodular/core/context.py +78 -0
  23. pymodular/core/controller.py +208 -0
  24. pymodular/core/error_handlers.py +272 -0
  25. pymodular/core/exceptions.py +104 -0
  26. pymodular/core/guards.py +117 -0
  27. pymodular/core/lifespan.py +150 -0
  28. pymodular/core/logging.py +88 -0
  29. pymodular/core/metrics.py +190 -0
  30. pymodular/core/schemas.py +105 -0
  31. pymodular/core/websocket/__init__.py +31 -0
  32. pymodular/core/websocket/adapter.py +192 -0
  33. pymodular/core/websocket/gateway.py +735 -0
  34. pymodular/core/websocket/namespace.py +148 -0
  35. pymodular/core/websocket/protocol.py +157 -0
  36. pymodular/core/websocket/server.py +175 -0
  37. pymodular/core/websocket/socket.py +241 -0
  38. pymodular/discovery.py +180 -0
  39. pymodular/factory.py +126 -0
  40. pymodular/infrastructure/__init__.py +1 -0
  41. pymodular/infrastructure/database/__init__.py +8 -0
  42. pymodular/infrastructure/database/base.py +228 -0
  43. pymodular/infrastructure/database/circuit.py +207 -0
  44. pymodular/infrastructure/database/factory.py +88 -0
  45. pymodular/infrastructure/database/memory.py +112 -0
  46. pymodular/infrastructure/database/mongo.py +186 -0
  47. pymodular/infrastructure/database/repository.py +188 -0
  48. pymodular/infrastructure/database/sql.py +520 -0
  49. pymodular/infrastructure/kafka/__init__.py +26 -0
  50. pymodular/infrastructure/kafka/broker.py +231 -0
  51. pymodular/infrastructure/kafka/consumers.py +371 -0
  52. pymodular/infrastructure/kafka/metrics.py +17 -0
  53. pymodular/infrastructure/mqtt/__init__.py +35 -0
  54. pymodular/infrastructure/mqtt/client.py +292 -0
  55. pymodular/infrastructure/mqtt/consumers.py +219 -0
  56. pymodular/infrastructure/mqtt/metrics.py +17 -0
  57. pymodular/infrastructure/mqtt/patterns.py +116 -0
  58. pymodular/infrastructure/rabbitmq/__init__.py +33 -0
  59. pymodular/infrastructure/rabbitmq/broker.py +616 -0
  60. pymodular/infrastructure/rabbitmq/consumers.py +450 -0
  61. pymodular/infrastructure/rabbitmq/metrics.py +34 -0
  62. pymodular/infrastructure/rabbitmq/patterns.py +64 -0
  63. pymodular/infrastructure/redis/__init__.py +31 -0
  64. pymodular/infrastructure/redis/client.py +362 -0
  65. pymodular/infrastructure/redis/metrics.py +20 -0
  66. pymodular/infrastructure/redis/pubsub.py +262 -0
  67. pymodular/middleware/__init__.py +0 -0
  68. pymodular/middleware/request_context.py +164 -0
  69. pymodular/py.typed +0 -0
@@ -0,0 +1,492 @@
1
+ """Sinh khung một module nghiệp vụ mới.
2
+
3
+ Chạy qua: pym module alerts [--entity Alert] [--gateway] [--consumer]
4
+ Thêm gateway cho module đã có: pym module alerts --gateway-only
5
+ Thêm consumer cho module đã có: pym module alerts --consumer-only
6
+
7
+ Tạo đúng cấu trúc của các module có sẵn — router / service / dto / entities —
8
+ với đầy đủ dây nối DI, decorator route và DTO tương ứng. Thân hàm để trống
9
+ kèm TODO, gọi vào sẽ trả 501 chứ không phải 500, để phân biệt "chưa viết" với
10
+ "có bug".
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import re
17
+ from pathlib import Path
18
+
19
+ DEFAULT_ROOT = Path("src/api")
20
+
21
+
22
+ # Đuôi kết thúc bằng "s" nhưng KHÔNG phải số nhiều: status, class, analysis...
23
+ _KHONG_PHAI_SO_NHIEU = ("ss", "us", "is", "as", "os")
24
+
25
+
26
+ def singular(plural: str) -> str:
27
+ """Đoán dạng số ít từ tên thư mục. Đoán sai thì truyền entity= để đè."""
28
+ if plural.endswith("ies"):
29
+ return plural[:-3] + "y"
30
+ if plural.endswith(("ses", "xes", "zes", "ches", "shes")):
31
+ return plural[:-2]
32
+ if plural.endswith("s") and not plural.endswith(_KHONG_PHAI_SO_NHIEU):
33
+ return plural[:-1]
34
+ return plural
35
+
36
+
37
+ def pascal(name: str) -> str:
38
+ return "".join(part.capitalize() for part in name.split("_") if part)
39
+
40
+
41
+ def render(module: str, entity: str) -> dict[str, str]:
42
+ """Trả về {đường dẫn tương đối: nội dung}."""
43
+ cls = pascal(entity) # Alert
44
+ var = entity # alert
45
+ files: dict[str, str] = {}
46
+
47
+ files["__init__.py"] = f'"""Module {cls}."""\n'
48
+ files["entities/__init__.py"] = ""
49
+ files["dto/__init__.py"] = ""
50
+
51
+ files[f"entities/{var}_model.py"] = f'''"""Entity của module {cls} — biểu diễn nội bộ, không trả thẳng ra HTTP."""
52
+
53
+ from __future__ import annotations
54
+
55
+ from dataclasses import dataclass, field
56
+ from datetime import datetime
57
+
58
+ from pymodular.core.clock import utcnow
59
+ from pymodular.core.container import entity
60
+
61
+
62
+ @entity(
63
+ # TODO: khai báo ràng buộc duy nhất và index cho các trường hay lọc.
64
+ # unique=["ma_dinh_danh", ("owner_id", "name")]
65
+ # indexes=[("owner_id", "created_at"), "status"]
66
+ # Ràng buộc duy nhất PHẢI khai ở đây; kiểm tra trong service là một cuộc đua.
67
+ )
68
+ @dataclass(slots=True)
69
+ class {cls}:
70
+ id: str
71
+
72
+ # TODO: thêm các trường của bạn ở đây. Trường có giá trị mặc định thì bản
73
+ # ghi cũ vẫn đọc được sau khi thêm cột (xem docs/database.md).
74
+ name: str = ""
75
+
76
+ created_at: datetime = field(default_factory=utcnow)
77
+ updated_at: datetime = field(default_factory=utcnow)
78
+ '''
79
+
80
+ files[f"dto/{var}_dto.py"] = f'''"""DTO vào/ra của module {cls}."""
81
+
82
+ from __future__ import annotations
83
+
84
+ from datetime import datetime
85
+
86
+ from pydantic import Field
87
+
88
+ from pymodular.core.schemas import InputSchema, OutputSchema, partial_of
89
+
90
+
91
+ class {cls}Base(InputSchema):
92
+ """Trường client được phép ghi, khai báo đúng MỘT lần."""
93
+
94
+ # TODO: thêm trường tương ứng với entity.
95
+ name: str = Field(min_length=1, max_length=100)
96
+
97
+
98
+ class {cls}Create({cls}Base):
99
+ """POST: mọi trường trong {cls}Base đều bắt buộc.
100
+
101
+ Trường chỉ đặt được lúc tạo (bất biến về sau) thì khai ở ĐÂY, không phải ở
102
+ {cls}Base — như vậy PATCH sẽ tự động từ chối chúng.
103
+ """
104
+
105
+
106
+ class {cls}Update(partial_of({cls}Base)):
107
+ """PATCH: mọi trường của {cls}Base thành optional, ràng buộc giữ nguyên.
108
+
109
+ Trường chỉ sửa được chứ không đặt lúc tạo thì thêm ở đây.
110
+ """
111
+
112
+
113
+ class {cls}Out(OutputSchema):
114
+ """Cố ý liệt kê tường minh: entity về sau có thể thêm trường nội bộ mà
115
+ không được lộ ra API."""
116
+
117
+ id: str
118
+ name: str
119
+ created_at: datetime
120
+ updated_at: datetime
121
+ '''
122
+
123
+ files[f"{var}_service.py"] = f'''"""Nghiệp vụ của module {cls} (@Service).
124
+
125
+ Chỉ tầng này chứa business rule. Ném lỗi nghiệp vụ (NotFoundError/ConflictError)
126
+ chứ không biết gì về HTTP status code.
127
+ """
128
+
129
+ from __future__ import annotations
130
+
131
+ from pymodular.core.container import injectable
132
+ from pymodular.core.logging import get_logger
133
+ from pymodular.infrastructure.database import Repository
134
+ from src.api.{module}.dto.{var}_dto import {cls}Create, {cls}Update
135
+ from src.api.{module}.entities.{var}_model import {cls}
136
+
137
+ log = get_logger(__name__)
138
+
139
+
140
+ @injectable
141
+ class {cls}Service:
142
+ def __init__(self, repo: Repository[{cls}]) -> None:
143
+ self._repo = repo
144
+
145
+ async def list_{module}(self, *, limit: int, offset: int) -> tuple[list[{cls}], int]:
146
+ # TODO: viết thân hàm. Gợi ý:
147
+ # return (
148
+ # await self._repo.find(limit=limit, offset=offset),
149
+ # await self._repo.count(),
150
+ # )
151
+ raise NotImplementedError("{cls}Service.list_{module} chưa được viết")
152
+
153
+ async def get_{var}(self, {var}_id: str) -> {cls}:
154
+ # TODO: gợi ý:
155
+ # item = await self._repo.get({var}_id)
156
+ # if item is None:
157
+ # raise NotFoundError(f"Không tìm thấy {var} {{{var}_id}}")
158
+ # return item
159
+ raise NotImplementedError("{cls}Service.get_{var} chưa được viết")
160
+
161
+ async def create_{var}(self, payload: {cls}Create) -> {cls}:
162
+ # TODO: gợi ý:
163
+ # return await self._repo.save({cls}(id="", **payload.model_dump()))
164
+ raise NotImplementedError("{cls}Service.create_{var} chưa được viết")
165
+
166
+ async def update_{var}(self, {var}_id: str, payload: {cls}Update) -> {cls}:
167
+ # TODO: gợi ý:
168
+ # item = await self.get_{var}({var}_id)
169
+ # apply_changes(item, payload) # updated_at do repository lo
170
+ # return await self._repo.save(item)
171
+ raise NotImplementedError("{cls}Service.update_{var} chưa được viết")
172
+
173
+ async def delete_{var}(self, {var}_id: str) -> None:
174
+ # TODO: gợi ý:
175
+ # if not await self._repo.delete({var}_id):
176
+ # raise NotFoundError(f"Không tìm thấy {var} {{{var}_id}}")
177
+ raise NotImplementedError("{cls}Service.delete_{var} chưa được viết")
178
+ '''
179
+
180
+ files[f"{var}_controller.py"] = f'''"""HTTP layer của module {cls} (@Controller).
181
+
182
+ Controller chỉ khai báo đường dẫn, validate qua schema, và đổi entity thành DTO.
183
+ Không có business rule ở đây.
184
+ """
185
+
186
+ from __future__ import annotations
187
+
188
+ from typing import Annotated
189
+
190
+ from fastapi import Path, Query, status
191
+
192
+ from pymodular.core.controller import controller, delete, get, patch, post
193
+ from pymodular.core.schemas import Page
194
+ from src.api.{module}.{var}_service import {cls}Service
195
+ from src.api.{module}.dto.{var}_dto import {cls}Create, {cls}Out, {cls}Update
196
+
197
+ {cls}Id = Annotated[str, Path(description="ID của {var}")]
198
+
199
+
200
+ @controller(prefix="/{module}", tags=["{module}"])
201
+ class {cls}Controller:
202
+ def __init__(self, service: {cls}Service) -> None:
203
+ self._service = service
204
+
205
+ @get("", response_model=Page[{cls}Out], summary="Danh sách {module}")
206
+ async def list_{module}(
207
+ self,
208
+ limit: Annotated[int, Query(ge=1, le=100)] = 20,
209
+ offset: Annotated[int, Query(ge=0)] = 0,
210
+ ) -> Page[{cls}Out]:
211
+ items, total = await self._service.list_{module}(limit=limit, offset=offset)
212
+ return Page(
213
+ items=[{cls}Out.model_validate(item) for item in items],
214
+ total=total,
215
+ limit=limit,
216
+ offset=offset,
217
+ )
218
+
219
+ @get("/{{{var}_id}}", response_model={cls}Out, summary="Chi tiết {var}")
220
+ async def get_{var}(self, {var}_id: {cls}Id) -> {cls}Out:
221
+ return {cls}Out.model_validate(await self._service.get_{var}({var}_id))
222
+
223
+ @post(
224
+ "",
225
+ response_model={cls}Out,
226
+ status_code=status.HTTP_201_CREATED,
227
+ summary="Tạo {var}",
228
+ )
229
+ async def create_{var}(self, payload: {cls}Create) -> {cls}Out:
230
+ return {cls}Out.model_validate(await self._service.create_{var}(payload))
231
+
232
+ @patch("/{{{var}_id}}", response_model={cls}Out, summary="Cập nhật {var}")
233
+ async def update_{var}(self, {var}_id: {cls}Id, payload: {cls}Update) -> {cls}Out:
234
+ return {cls}Out.model_validate(
235
+ await self._service.update_{var}({var}_id, payload)
236
+ )
237
+
238
+ @delete(
239
+ "/{{{var}_id}}",
240
+ status_code=status.HTTP_204_NO_CONTENT,
241
+ summary="Xoá {var}",
242
+ )
243
+ async def delete_{var}(self, {var}_id: {cls}Id) -> None:
244
+ await self._service.delete_{var}({var}_id)
245
+ '''
246
+ return files
247
+
248
+
249
+ def render_gateway(module: str, entity: str) -> dict[str, str]:
250
+ """Khung gateway WebSocket cho một module. Trả về {đường dẫn tương đối: nội dung}."""
251
+ cls = pascal(entity)
252
+ var = entity
253
+ files: dict[str, str] = {}
254
+
255
+ files[f"dto/{var}_ws_dto.py"] = f'''"""DTO cho các sự kiện WebSocket của module {cls}.
256
+
257
+ Payload WebSocket được validate bằng chính pydantic như body HTTP: sai thì
258
+ client nhận khung `error` mang code `validation_error`, không phải 500.
259
+ """
260
+
261
+ from __future__ import annotations
262
+
263
+ from pydantic import Field
264
+
265
+ from pymodular.core.schemas import InputSchema
266
+
267
+
268
+ class {cls}Event(InputSchema):
269
+ """Dữ liệu client gửi lên kèm sự kiện."""
270
+
271
+ # TODO: thêm trường của bạn.
272
+ room: str = Field(min_length=1, max_length=128)
273
+ '''
274
+
275
+ files[f"{var}_gateway.py"] = f'''"""Gateway WebSocket của module {cls} (@WebSocketGateway).
276
+
277
+ Kết nối: ws://localhost:8000/ws/{module}?client_id=an
278
+
279
+ Không phải đăng ký ở đâu cả — app/app.py tự quét và gắn. Xem
280
+ docs/websocket.md để biết khuôn tin nhắn, cách gửi cho phòng / cho một người,
281
+ và ví dụ client Postman + Next.js.
282
+ """
283
+
284
+ from __future__ import annotations
285
+
286
+ from typing import Any
287
+
288
+ from pymodular.core.guards import RequireHeader
289
+ from pymodular.core.logging import get_logger
290
+ from pymodular.core.websocket import Socket, WebSocketServer, gateway, subscribe
291
+ from src.api.{module}.dto.{var}_ws_dto import {cls}Event
292
+
293
+ log = get_logger(__name__)
294
+
295
+
296
+ @gateway(
297
+ path="/ws/{module}",
298
+ guards=[RequireHeader], # TODO: đổi sang guard xác thực thật của bạn
299
+ # client_rooms=True thì client tự gửi được room.join/room.leave. Bật thì
300
+ # NHỚ viết can_join() bên dưới, nếu không ai cũng vào được phòng của người khác.
301
+ client_rooms=False,
302
+ )
303
+ class {cls}Gateway:
304
+ def __init__(self, server: WebSocketServer) -> None:
305
+ # Dùng để đẩy tin: server.to_room(...) / to_user(...) / to_socket(...)
306
+ self._server = server
307
+
308
+ # ------------------------------------------------------------ vòng đời
309
+ async def on_connect(self, socket: Socket) -> None:
310
+ """Chạy sau khi guard cho qua, trước khi client nhận khung `connected`."""
311
+ # TODO: gợi ý — cho mỗi người một phòng riêng để gửi thông báo cá nhân:
312
+ # if socket.user_id:
313
+ # socket.join(f"user:{{socket.user_id}}")
314
+ log.info("{module}.connected", socket_id=socket.id, user_id=socket.user_id)
315
+
316
+ async def on_disconnect(self, socket: Socket, code: int) -> None:
317
+ """Chạy khi kết nối đứt, dù vì lý do gì. Sổ phòng đã tự dọn."""
318
+ log.info("{module}.disconnected", socket_id=socket.id, code=code)
319
+
320
+ # def can_join(self, socket: Socket, room: str) -> bool:
321
+ # """Chốt chặn cho room.join do client gửi lên (cần client_rooms=True)."""
322
+ # return room == f"user:{{socket.user_id}}"
323
+
324
+ # -------------------------------------------------------------- sự kiện
325
+ @subscribe("{var}.subscribe")
326
+ async def subscribe_{var}(self, socket: Socket, payload: {cls}Event) -> dict[str, Any]:
327
+ """Giá trị trả về được gửi lại làm ack khi client có kèm `id`."""
328
+ # TODO: gợi ý:
329
+ # socket.join(payload.room)
330
+ # return {{"room": payload.room, "size": socket.namespace.room_size(payload.room)}}
331
+ raise NotImplementedError("{cls}Gateway.subscribe_{var} chưa được viết")
332
+
333
+ @subscribe("{var}.ping")
334
+ async def ping_{var}(self, socket: Socket) -> dict[str, Any]:
335
+ """Handler không cần payload thì bỏ luôn tham số thứ hai."""
336
+ # TODO: gợi ý:
337
+ # return {{"socket_id": socket.id, "rooms": sorted(socket.rooms)}}
338
+ raise NotImplementedError("{cls}Gateway.ping_{var} chưa được viết")
339
+ '''
340
+ return files
341
+
342
+
343
+ def _write(target: Path, files: dict[str, str]) -> None:
344
+ for relative, content in files.items():
345
+ path = target / relative
346
+ path.parent.mkdir(parents=True, exist_ok=True)
347
+ path.write_text(content, encoding="utf-8")
348
+
349
+
350
+ def render_consumer(module: str, entity: str) -> dict[str, str]:
351
+ """Khung consumer RabbitMQ cho một module."""
352
+ cls = pascal(entity)
353
+ var = entity
354
+
355
+ return {
356
+ f"{var}_consumer.py": f'''"""Consumer nền của module {cls} (@EventPattern).
357
+
358
+ Hàng đợi BỀN và có TÊN, nên nhiều worker chia nhau xử lý — mỗi tin đúng một
359
+ worker làm. Dùng cho việc phải làm ĐÚNG MỘT LẦN: gửi mail, ghi sổ, gọi dịch vụ
360
+ ngoài. Cần đẩy tin cho MỌI worker (ví dụ xuống WebSocket) thì dùng cầu nối
361
+ `event.subscribe`, không phải chỗ này — xem docs/rabbitmq.md.
362
+
363
+ RabbitMQ tắt (mặc định) thì file này nằm im, không tạo hàng đợi nào.
364
+ """
365
+
366
+ from __future__ import annotations
367
+
368
+ from typing import Any
369
+
370
+ from pymodular.core.container import injectable
371
+ from pymodular.core.logging import get_logger
372
+ from pymodular.infrastructure.rabbitmq import rabbitmq_subscriber
373
+ from src.api.{module}.{var}_service import {cls}Service
374
+
375
+ log = get_logger(__name__)
376
+
377
+
378
+ @injectable
379
+ class {cls}Consumer:
380
+ def __init__(self, service: {cls}Service) -> None:
381
+ self._service = service
382
+
383
+ # Mọi chính sách của consumer khai ngay ở đây, không phải trong .env:
384
+ # max_retries=5, retry_delay=60, dead_letter=False, durable=False, prefetch=200
385
+ # Mặc định: đúng MỘT hàng đợi. Thêm max_retries=3, dead_letter=True nếu tin
386
+ # này đáng tiền (đơn hàng, thanh toán) — khi đó mới có <queue>.retry/.dlq.
387
+ @rabbitmq_subscriber("events", "{var}.#", queue="{module}-worker")
388
+ async def handle_{var}(self, payload: dict, meta: dict[str, Any]) -> None:
389
+ """Nhận mọi sự kiện `{var}.*` trên exchange `events`.
390
+
391
+ Tham số `meta` là tuỳ chọn: bỏ đi nếu không cần routing key thật, số
392
+ lần đã thử, hay message id.
393
+
394
+ Ném lỗi thường -> thử lại tối đa `max_retries` lần (mặc định 3) rồi
395
+ vào hàng đợi chết `{module}-worker.dlq`.
396
+ Ném PermanentMessageError -> vào thẳng hàng đợi chết, không thử lại.
397
+ """
398
+ # TODO: gợi ý:
399
+ # log.info("{var}.received", routing_key=meta["routing_key"])
400
+ # await self._service.get_{var}(payload["id"])
401
+ raise NotImplementedError("{cls}Consumer.handle_{var} chưa được viết")
402
+ '''
403
+ }
404
+
405
+
406
+ def main(argv: list[str] | None = None) -> int:
407
+ parser = argparse.ArgumentParser(description="Sinh khung module nghiệp vụ")
408
+ parser.add_argument("name", help="tên module, dạng số nhiều viết thường: alerts")
409
+ parser.add_argument("--entity", help="tên entity dạng số ít; mặc định đoán từ name")
410
+ parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
411
+ parser.add_argument(
412
+ "--gateway", action="store_true", help="tạo kèm gateway WebSocket"
413
+ )
414
+ parser.add_argument(
415
+ "--gateway-only",
416
+ action="store_true",
417
+ help="chỉ thêm gateway WebSocket vào module đã có",
418
+ )
419
+ parser.add_argument(
420
+ "--consumer", action="store_true", help="tạo kèm consumer RabbitMQ"
421
+ )
422
+ parser.add_argument(
423
+ "--consumer-only",
424
+ action="store_true",
425
+ help="chỉ thêm consumer RabbitMQ vào module đã có",
426
+ )
427
+ args = parser.parse_args(argv)
428
+
429
+ module = args.name.strip().lower().replace("-", "_")
430
+ if not re.fullmatch(r"[a-z][a-z0-9_]*", module):
431
+ print(f"Tên module không hợp lệ: {args.name!r}. Chỉ dùng chữ thường, số và _.")
432
+ return 1
433
+
434
+ entity = (args.entity or singular(module)).strip().lower().replace("-", "_")
435
+
436
+ target = args.root / module
437
+
438
+ if args.gateway_only or args.consumer_only:
439
+ if not target.exists():
440
+ print(f"Chưa có module {target}. Tạo trước bằng: pym module {module}")
441
+ return 1
442
+ files = render_gateway(module, entity) if args.gateway_only else render_consumer(module, entity)
443
+ trung = [rel for rel in files if (target / rel).exists()]
444
+ if trung:
445
+ print(f"Đã có sẵn: {', '.join(str(target / rel) for rel in trung)}")
446
+ return 1
447
+ _write(target, files)
448
+ loai = "gateway WebSocket" if args.gateway_only else "consumer RabbitMQ"
449
+ print(f"Đã thêm {loai} vào module '{module}':")
450
+ for relative in sorted(files):
451
+ print(f" {target / relative}")
452
+ print()
453
+ print("Việc tiếp theo:")
454
+ if args.gateway_only:
455
+ print(f" 1. Viết thân các handler trong {entity}_gateway.py")
456
+ print(f" 2. pym dev, rồi nối thử: ws://localhost:8000/ws/{module}?client_id=an")
457
+ print(" 3. Xem docs/websocket.md (có ví dụ Postman và Next.js)")
458
+ else:
459
+ print(f" 1. Viết thân handler trong {entity}_consumer.py")
460
+ print(" 2. pip install \'fastapi-modular[rabbitmq]\' rồi APP_RABBITMQ__ENABLED=true")
461
+ print(" 3. Xem docs/rabbitmq.md")
462
+ return 0
463
+
464
+ if target.exists():
465
+ print(f"Đã có {target} rồi. Xoá đi hoặc chọn tên khác.")
466
+ return 1
467
+
468
+ files = render(module, entity)
469
+ if args.gateway:
470
+ files.update(render_gateway(module, entity))
471
+ if args.consumer:
472
+ files.update(render_consumer(module, entity))
473
+ _write(target, files)
474
+
475
+ print(f"Đã tạo module '{module}' (entity {pascal(entity)}) tại {target}:")
476
+ for relative in sorted(files):
477
+ print(f" {target / relative}")
478
+ print()
479
+ print("Việc tiếp theo:")
480
+ print(f" 1. Thêm trường vào entities/{entity}_model.py và dto/{entity}_dto.py")
481
+ print(" 2. Khai unique/indexes trong @entity nếu cần")
482
+ print(f" 3. Viết thân các hàm trong {entity}_service.py (đang raise NotImplementedError)")
483
+ print(" 4. pym dev — route đã tự xuất hiện, không phải đăng ký ở đâu cả")
484
+ if args.gateway:
485
+ print(f" 5. Viết thân handler trong {entity}_gateway.py (xem docs/websocket.md)")
486
+ if args.consumer:
487
+ print(f" 6. Viết thân handler trong {entity}_consumer.py (xem docs/rabbitmq.md)")
488
+ return 0
489
+
490
+
491
+ if __name__ == "__main__":
492
+ raise SystemExit(main())