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,471 @@
1
+ """Dựng một dự án chạy được ngay.
2
+
3
+ pym new blog tạo thư mục blog/ rồi đổ file vào đó
4
+ pym init đổ file vào THƯ MỤC HIỆN TẠI
5
+
6
+ Bộ khung sinh ra:
7
+
8
+ src/main.py điểm vào — lắp ráp app, file của bạn, sửa thoải mái
9
+ src/core/config.py cấu hình: kế thừa Settings để thêm biến .env của bạn
10
+ src/core/lifespan.py việc lúc khởi động / lúc tắt của riêng ứng dụng
11
+ src/api/health/ một module mẫu
12
+
13
+ Mọi thứ khác (module nghiệp vụ, gateway, consumer) sinh sau bằng `pym module`.
14
+
15
+ `init` không bao giờ GHI ĐÈ: file nào đã có thì bỏ qua và báo lại. Nhờ vậy chạy
16
+ nó trong một thư mục đang có sẵn code là an toàn, và chạy lại lần hai chỉ bù
17
+ những file còn thiếu.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ import unicodedata
24
+ from pathlib import Path
25
+
26
+ TEN_HOP_LE = re.compile(r"^[a-z][a-z0-9_-]*$")
27
+
28
+ REPO = "https://github.com/quanglinh2909/pymodular"
29
+
30
+ GOC = '''"""Ứng dụng — code của bạn.
31
+
32
+ src/main.py điểm vào: lắp ráp app, sửa thoải mái
33
+ src/core/ thứ dùng chung của ứng dụng (config, helper, guard riêng)
34
+ src/api/ các module nghiệp vụ; mỗi thư mục con là một module
35
+ """
36
+ '''
37
+
38
+ LIFESPAN = '''"""Vòng đời ứng dụng — FILE CỦA BẠN.
39
+
40
+ Khung lo phần hạ tầng: mở/đóng database, WebSocket, và những lớp hàng đợi đang
41
+ bật (RabbitMQ, Redis, MQTT, Kafka). Việc RIÊNG của ứng dụng — nạp cache, hâm
42
+ nóng model, đăng ký với service discovery, đóng sổ khi tắt — viết ở đây.
43
+
44
+ Thứ tự quan trọng và cố ý:
45
+
46
+ khung mở database, hàng đợi
47
+ -> việc khởi động của bạn (đã có database để dùng)
48
+ -> app phục vụ request
49
+ -> việc lúc tắt của bạn (database VẪN CÒN để ghi nốt)
50
+ khung đóng hàng đợi, database
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ from collections.abc import AsyncIterator
56
+ from contextlib import asynccontextmanager
57
+
58
+ from fastapi import FastAPI
59
+ from pymodular import get_logger
60
+ from pymodular import lifespan as framework_lifespan
61
+
62
+ log = get_logger(__name__)
63
+
64
+
65
+ @asynccontextmanager
66
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
67
+ async with framework_lifespan(app):
68
+ # --- KHỞI ĐỘNG: chạy sau khi database và hàng đợi đã sẵn sàng ---
69
+ log.info("app.ready")
70
+
71
+ try:
72
+ yield
73
+ finally:
74
+ # --- TẮT: chạy trước khi khung đóng database ---
75
+ log.info("app.closing")
76
+ '''
77
+
78
+ MAIN = '''"""Điểm vào — chạy bằng `pym dev`.
79
+
80
+ FILE NÀY LÀ CỦA BẠN. Khung cố ý không giấu phần lắp ráp: mỗi dòng dưới đây làm
81
+ đúng một việc, xoá được, đổi thứ tự được, chèn thêm được.
82
+
83
+ Thêm module nghiệp vụ thì KHÔNG phải sửa file này — `register_routes` tự quét
84
+ thư mục `src/api/`. Còn thêm middleware, đổi CORS, gắn router của thư viện
85
+ ngoài, bọc lifespan... thì sửa ngay tại đây.
86
+
87
+ Chưa cần sửa gì thì cả khối dưới rút lại còn hai dòng:
88
+
89
+ from pymodular import create_app
90
+ app = create_app(AppSettings())
91
+ """
92
+
93
+ from __future__ import annotations
94
+
95
+ from pymodular import (
96
+ add_middleware,
97
+ bind_settings,
98
+ configure_logging,
99
+ new_fastapi,
100
+ register_error_handlers,
101
+ register_routes,
102
+ )
103
+
104
+ from src.core.config import AppSettings
105
+ from src.core.lifespan import lifespan
106
+
107
+ # Đọc .env, chốt lớp cấu hình cho cả tiến trình, cắm vào DI container.
108
+ settings = bind_settings(AppSettings())
109
+ configure_logging(settings.log)
110
+
111
+ # lifespan: khung lo database và hạ tầng, phần việc riêng nằm ở
112
+ # src/core/lifespan.py — sửa ở đó, không phải ở đây.
113
+ app = new_fastapi(settings, lifespan=lifespan)
114
+
115
+ # CORS + access log + request-id. Middleware của bạn thêm sau dòng này sẽ chạy
116
+ # TRƯỚC ba cái đó (FastAPI chạy ngược thứ tự add).
117
+ add_middleware(app, settings)
118
+
119
+ # Đổi lỗi nghiệp vụ thành JSON có mã và request_id.
120
+ register_error_handlers(app, debug=settings.debug)
121
+
122
+ # Quét app/, gắn mọi @controller và @gateway tìm được.
123
+ register_routes(app, prefix=settings.api_prefix)
124
+
125
+
126
+ @app.get("/", include_in_schema=False)
127
+ async def root() -> dict[str, str]:
128
+ return {"service": settings.name, "version": settings.version}
129
+ '''
130
+
131
+ CONFIG = '''"""Cấu hình của ứng dụng — thêm biến `.env` của riêng bạn ở đây.
132
+
133
+ `Settings` mang sẵn phần của khung (database, WebSocket, RabbitMQ, Redis, MQTT,
134
+ Kafka, log, CORS). Kế thừa nó là đủ để pydantic-settings đọc thêm biến của bạn,
135
+ theo đúng quy tắc cũ: biến môi trường thắng .env, .env thắng giá trị mặc định,
136
+ nhóm lồng nhau ngăn bằng hai gạch dưới.
137
+
138
+ class JwtSettings(BaseModel): # -> APP_JWT__SECRET, APP_JWT__TTL_SECONDS
139
+ secret: str = ""
140
+ ttl_seconds: int = 3600
141
+
142
+ class AppSettings(Settings):
143
+ jwt: JwtSettings = Field(default_factory=JwtSettings, alias="APP_JWT")
144
+
145
+ Service nhận nó qua DI bằng chính lớp con, và vẫn có gợi ý kiểu đầy đủ:
146
+
147
+ @injectable
148
+ class TokenService:
149
+ def __init__(self, settings: AppSettings) -> None:
150
+ self._secret = settings.jwt.secret
151
+ """
152
+
153
+ from __future__ import annotations
154
+
155
+ from pydantic import Field
156
+
157
+ from pymodular import Settings
158
+
159
+
160
+ class AppSettings(Settings):
161
+ """Settings của khung, cộng thêm phần của ứng dụng."""
162
+
163
+ # Ví dụ một biến riêng — đọc từ APP_TEAM_NAME trong .env. Xoá được.
164
+ team_name: str = Field(default="", alias="APP_TEAM_NAME")
165
+ '''
166
+
167
+ HEALTH = '''"""Module mẫu — xoá được, hoặc giữ lại làm endpoint kiểm tra sức khoẻ."""
168
+
169
+ from __future__ import annotations
170
+
171
+ from pymodular import Settings, controller, get
172
+
173
+
174
+ @controller(prefix="/health", tags=["health"])
175
+ class HealthController:
176
+ def __init__(self, settings: Settings) -> None:
177
+ self._settings = settings
178
+
179
+ @get("", summary="Tiến trình còn sống")
180
+ async def live(self) -> dict[str, str]:
181
+ return {"status": "ok", "service": self._settings.name}
182
+ '''
183
+
184
+ ENV = """APP_NAME={ten}
185
+ APP_ENV=local
186
+ APP_DEBUG=true
187
+ APP_HOST=0.0.0.0
188
+ APP_PORT=8000
189
+
190
+ # Chưa chọn database thì app chạy bằng bộ nhớ tạm (mất dữ liệu khi restart).
191
+ # Thêm database: pym install sqlite (hoặc postgres, mongodb)
192
+ # Thêm hàng đợi: pym install rabbitmq (hoặc redis, mqtt, kafka)
193
+ # Mỗi lệnh vừa cài thư viện, vừa ghi biến vào file này kèm giải thích.
194
+ #
195
+ # Biến của RIÊNG bạn: thêm thẳng vào đây, rồi khai trong src/core/config.py.
196
+ # APP_TEAM_NAME=to-backend
197
+ """
198
+
199
+ GITIGNORE = """# ---------------------------------------------------------------- Python
200
+ __pycache__/
201
+ *.py[cod]
202
+ *$py.class
203
+ *.so
204
+ .Python
205
+
206
+ # Gói và bản dựng
207
+ build/
208
+ dist/
209
+ sdist/
210
+ wheels/
211
+ *.egg
212
+ *.egg-info
213
+ .eggs/
214
+ MANIFEST
215
+
216
+ # Môi trường ảo
217
+ .venv/
218
+ venv/
219
+ ENV/
220
+ env/
221
+ .python-version
222
+
223
+ # Test, độ phủ, kiểm kiểu, lint
224
+ .pytest_cache/
225
+ .ruff_cache/
226
+ .mypy_cache/
227
+ .dmypy.json
228
+ .pytype/
229
+ .pyre/
230
+ .tox/
231
+ .nox/
232
+ .coverage
233
+ .coverage.*
234
+ coverage.xml
235
+ htmlcov/
236
+ *.cover
237
+ .hypothesis/
238
+ .cache/
239
+
240
+ # Jupyter
241
+ .ipynb_checkpoints/
242
+
243
+ # ------------------------------------------------------------- Dự án này
244
+ # .env chứa DSN và mật khẩu thật — KHÔNG BAO GIỜ commit.
245
+ .env
246
+ .env.*
247
+ !.env.example
248
+
249
+ data/
250
+ *.db
251
+ *.sqlite
252
+ *.sqlite3
253
+ *.log
254
+ logs/
255
+
256
+ # ------------------------------------------------------------------- IDE
257
+ .idea/
258
+ .vscode/
259
+ .fleet/
260
+ .zed/
261
+ *.sublime-project
262
+ *.sublime-workspace
263
+ *.swp
264
+ *.swo
265
+ *~
266
+
267
+ # --------------------------------------------------------- Hệ điều hành
268
+ .DS_Store
269
+ ._*
270
+ Thumbs.db
271
+ Desktop.ini
272
+
273
+ # --------------------------------------------------------------- Công cụ
274
+ .direnv/
275
+ node_modules/
276
+ """
277
+
278
+ README = """# {ten}
279
+
280
+ Dựng bằng [pymodular]({repo}) — FastAPI theo kiến trúc module kiểu NestJS.
281
+
282
+ ## Chạy
283
+
284
+ ```bash
285
+ python -m venv .venv && . .venv/bin/activate
286
+ pip install fastapi-modular
287
+ pym dev # http://localhost:8000/docs
288
+ ```
289
+
290
+ ## Cấu trúc
291
+
292
+ ```
293
+ src/
294
+ ├── main.py điểm vào: lắp ráp app — sửa thoải mái, không phải của khung
295
+ ├── core/
296
+ │ ├── config.py AppSettings — thêm biến .env của riêng bạn
297
+ │ └── lifespan.py việc lúc khởi động / lúc tắt của riêng bạn
298
+ └── api/ mỗi thư mục con là một module; thêm module KHÔNG phải sửa main.py
299
+ └── health/
300
+ ```
301
+
302
+ ## Lệnh
303
+
304
+ Rút gọn được tới khi nào tiền tố còn chỉ đúng một lệnh: `pym mo alerts` chạy y
305
+ hệt `pym module alerts`. Nhập nhằng thì `pym` hỏi lại chứ không đoán.
306
+
307
+ | Lệnh | Rút gọn | Làm gì |
308
+ |---|---|---|
309
+ | `pym dev` | `pym d` | chạy kèm autoreload |
310
+ | `pym run --workers 4` | `pym r` | chạy chế độ production |
311
+ | `pym module <tên>` | `pym mo` | sinh module: controller + service + dto + entity |
312
+ | `pym module <tên> --gateway` | | kèm gateway WebSocket (`--consumer` cho RabbitMQ) |
313
+ | `pym env <thành-phần>` | `pym e` | chỉ ghi biến vào `.env`, không cài gì |
314
+ | `pym info` | `pym inf` | đang nối vào đâu, thư viện nào đã cài |
315
+ | `pym migrate` | `pym mi` | chạy migration (Alembic) |
316
+ | `pym test` · `pym lint` | `pym t` · `pym l` | pytest · ruff |
317
+ | `pym clean` | `pym c` | xoá cache và bản dựng (không đụng `data/`) |
318
+ | **Thêm database** | | *cài thư viện **rồi** ghi biến vào `.env`* |
319
+ | `pym install sqlite` | `pym ins s` | file `.db`, không cần server |
320
+ | `pym install postgres` | `pym ins p` | PostgreSQL |
321
+ | `pym install mongodb` | `pym ins mo` | MongoDB |
322
+ | **Thêm hàng đợi** | | *cài thư viện **rồi** ghi biến vào `.env`* |
323
+ | `pym install rabbitmq` | `pym ins ra` | hàng đợi bền, thử lại + DLQ |
324
+ | `pym install redis` | `pym ins re` | cache, đếm nguyên tử, pub/sub |
325
+ | `pym install mqtt` | `pym ins mq` | thiết bị IoT |
326
+ | `pym install kafka` | `pym ins k` | nhật ký sự kiện đọc lại được |
327
+ | `pym install ws-redis` | `pym ins w` | phát tin WebSocket xuyên nhiều worker |
328
+ | `pym install all` | `pym ins a` | tất cả những thứ trên |
329
+
330
+ `pym --help` cho danh sách đầy đủ. Host và cổng lấy từ `APP_HOST` / `APP_PORT`
331
+ trong `.env`.
332
+
333
+ `pym install` ghi vào `.env` mỗi biến kèm giải thích, cho biết nó **bắt buộc hay
334
+ tuỳ chọn** và **mặc định là gì** nếu xoá dòng đi. Không cài, không bật thì lớp đó
335
+ nằm im — không import thư viện, không mở kết nối, không đổi hành vi nào.
336
+
337
+ ## Thêm module
338
+
339
+ ```bash
340
+ pym module {vi_du} # controller + service + dto + entity
341
+ pym module {vi_du} --gateway # kèm gateway WebSocket
342
+ pym module {vi_du} --consumer # kèm consumer RabbitMQ
343
+ ```
344
+
345
+ Route xuất hiện ngay; chỉ thân hàm trong service là chưa viết (gọi vào trả 501
346
+ kèm tên hàm).
347
+
348
+ ## Thêm biến cấu hình của riêng bạn
349
+
350
+ ```python
351
+ # src/core/config.py
352
+ from pydantic import Field
353
+
354
+ from pymodular import Settings
355
+
356
+
357
+ class AppSettings(Settings):
358
+ team_name: str = Field(default="", alias="APP_TEAM_NAME")
359
+ ```
360
+
361
+ ```bash
362
+ # .env
363
+ APP_TEAM_NAME=to-backend
364
+ ```
365
+
366
+ Service nhận nó qua DI bằng chính lớp con:
367
+
368
+ ```python
369
+ @injectable
370
+ class TokenService:
371
+ def __init__(self, settings: AppSettings) -> None:
372
+ self._team = settings.team_name
373
+ ```
374
+
375
+ ## Tài liệu
376
+
377
+ [{repo}/tree/main/docs]({repo}/tree/main/docs) — database, WebSocket, RabbitMQ,
378
+ Redis, MQTT, Kafka, cấu hình, vận hành.
379
+ """
380
+
381
+
382
+ def lam_sach_ten(tho: str) -> str:
383
+ """Tên thư mục -> tên dự án dùng được: "Dự Án Mới" -> "du-an-moi"."""
384
+ bo_dau = unicodedata.normalize("NFD", tho).encode("ascii", "ignore").decode()
385
+ gon = re.sub(r"[^a-zA-Z0-9]+", "-", bo_dau).strip("-").lower()
386
+ return gon or "app"
387
+
388
+
389
+ def tao_du_an(ten: str, root: Path) -> int:
390
+ """`new`: tạo thư mục mới rồi đổ file vào."""
391
+ if not TEN_HOP_LE.match(ten):
392
+ print(f"Tên dự án không hợp lệ: {ten!r}. Chữ thường, số, gạch ngang hoặc gạch dưới.")
393
+ return 1
394
+
395
+ dich = root / ten
396
+ if dich.exists() and any(dich.iterdir()):
397
+ print(
398
+ f"{dich} đã tồn tại và không rỗng — chọn tên khác, xoá nó trước, "
399
+ f"hoặc vào trong đó chạy `pym init`."
400
+ )
401
+ return 1
402
+
403
+ so = _ghi(dich, ten, ghi_de=True)
404
+ print(f"Đã tạo {dich}/ với {so} file:")
405
+ _in_cay(dich, ten)
406
+ print(f"\nChạy thử:\n cd {ten}\n pip install fastapi-modular\n pym dev")
407
+ print("\nRồi mở http://localhost:8000/docs")
408
+ return 0
409
+
410
+
411
+ def init_du_an(root: Path, ten: str | None = None) -> int:
412
+ """`init`: đổ file vào THƯ MỤC HIỆN TẠI, không tạo thêm một cấp.
413
+
414
+ Không ghi đè file nào đã có — thư mục đang có code vẫn chạy được lệnh này.
415
+ """
416
+ dich = root.resolve()
417
+ ten = ten or lam_sach_ten(dich.name)
418
+ if not TEN_HOP_LE.match(ten):
419
+ print(f"Tên dự án không hợp lệ: {ten!r}. Dùng --name để đặt tên khác.")
420
+ return 1
421
+
422
+ da_co = [d for d in _noi_dung(ten) if (dich / d).exists()]
423
+ so = _ghi(dich, ten, ghi_de=False)
424
+
425
+ if not so:
426
+ print(f"{dich} đã có đủ file rồi, không phải làm gì.")
427
+ return 0
428
+
429
+ print(f"Đã thêm {so} file vào {dich} (tên dự án: {ten}):")
430
+ _in_cay(dich, ten, bo_qua=set(da_co))
431
+ if da_co:
432
+ print("\nGiữ nguyên file đã có, KHÔNG ghi đè:")
433
+ for d in da_co:
434
+ print(f" {d}")
435
+ print("\nChạy thử:\n pip install fastapi-modular\n pym dev")
436
+ return 0
437
+
438
+
439
+ def _noi_dung(ten: str) -> dict[str, str]:
440
+ return {
441
+ "src/__init__.py": GOC,
442
+ "src/main.py": MAIN,
443
+ "src/core/__init__.py": '"""Thứ dùng chung của ứng dụng: config, helper, guard riêng."""\n',
444
+ "src/core/config.py": CONFIG,
445
+ "src/core/lifespan.py": LIFESPAN,
446
+ "src/api/__init__.py": '"""Các module nghiệp vụ; mỗi thư mục con là một module."""\n',
447
+ "src/api/health/__init__.py": '"""Module health."""\n',
448
+ "src/api/health/health_controller.py": HEALTH,
449
+ ".env": ENV.format(ten=ten),
450
+ ".gitignore": GITIGNORE,
451
+ "README.md": README.format(ten=ten, vi_du="alerts", repo=REPO),
452
+ }
453
+
454
+
455
+ def _ghi(dich: Path, ten: str, *, ghi_de: bool) -> int:
456
+ """Ghi file, trả về số file thật sự được tạo."""
457
+ so = 0
458
+ for duong_dan, noi_dung in _noi_dung(ten).items():
459
+ f = dich / duong_dan
460
+ if f.exists() and not ghi_de:
461
+ continue
462
+ f.parent.mkdir(parents=True, exist_ok=True)
463
+ f.write_text(noi_dung, encoding="utf-8")
464
+ so += 1
465
+ return so
466
+
467
+
468
+ def _in_cay(dich: Path, ten: str, bo_qua: set[str] | None = None) -> None:
469
+ for duong_dan in _noi_dung(ten):
470
+ if not bo_qua or duong_dan not in bo_qua:
471
+ print(f" {duong_dan}")
pymodular/cli/serve.py ADDED
@@ -0,0 +1,59 @@
1
+ """`pym dev` và `pym run` — chạy ứng dụng mà không phải nhớ dòng uvicorn nào.
2
+
3
+ Khác nhau đúng một chỗ: `dev` bật autoreload và chạy MỘT tiến trình, `run` tắt
4
+ reload và chạy nhiều worker. Host/cổng lấy từ cấu hình (`APP_HOST`, `APP_PORT`)
5
+ nên đổi trong .env là đủ, không phải sửa lệnh.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ def _chuan_bi_sys_path() -> None:
15
+ """Thêm thư mục hiện tại vào sys.path.
16
+
17
+ Lệnh `uvicorn` tự làm việc này, nhưng khi gọi uvicorn từ trong một
18
+ console_script thì sys.path[0] là thư mục chứa script trong venv, không phải
19
+ dự án — nên `src.main` sẽ không import được.
20
+ """
21
+ cwd = str(Path.cwd())
22
+ if cwd not in sys.path:
23
+ sys.path.insert(0, cwd)
24
+
25
+
26
+ def _thieu_app(target: str) -> int:
27
+ ten_module = target.split(":")[0]
28
+ print(
29
+ f"Không import được {ten_module!r} từ {Path.cwd()}.\n"
30
+ "Đứng ở thư mục gốc dự án (chỗ có app/), hoặc chỉ đường bằng --app.\n"
31
+ "Chưa có dự án thì tạo bằng: pym init"
32
+ )
33
+ return 1
34
+
35
+
36
+ def serve(*, target: str, reload: bool, workers: int | None, host: str | None,
37
+ port: int | None) -> int:
38
+ _chuan_bi_sys_path()
39
+
40
+ import uvicorn
41
+
42
+ from pymodular.core.config import get_settings
43
+
44
+ settings = get_settings()
45
+ host = host or settings.host
46
+ port = port or settings.port
47
+
48
+ ten_module = target.split(":")[0]
49
+ goc = ten_module.split(".")[0]
50
+ if not (Path.cwd() / goc).exists() and not (Path.cwd() / f"{goc}.py").exists():
51
+ return _thieu_app(target)
52
+
53
+ if reload:
54
+ # Chỉ theo dõi package ứng dụng: theo dõi cả cây thư mục sẽ khiến mỗi
55
+ # lần ghi file .db hay .env cũng khởi động lại server.
56
+ uvicorn.run(target, host=host, port=port, reload=True, reload_dirs=[goc])
57
+ else:
58
+ uvicorn.run(target, host=host, port=port, workers=workers or 4)
59
+ return 0
File without changes
@@ -0,0 +1,15 @@
1
+ """Nguồn thời gian dùng chung.
2
+
3
+ Tách riêng để (a) không module nào phải import module khác chỉ vì cần lấy giờ,
4
+ (b) test có thể monkeypatch một chỗ duy nhất khi cần đóng băng thời gian.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+
11
+ from pymodular.core.compat import UTC
12
+
13
+
14
+ def utcnow() -> datetime:
15
+ return datetime.now(UTC)
@@ -0,0 +1,39 @@
1
+ """Vá những khác biệt giữa các phiên bản Python được hỗ trợ (3.10+).
2
+
3
+ Gom vào một chỗ để phần còn lại của khung viết như thể chỉ có một phiên bản.
4
+ Bỏ hỗ trợ 3.10 thì xoá file này và sửa hai chỗ import — không phải lần mò khắp
5
+ nơi tìm xem cái gì cần bao nhiêu.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import sys
12
+ from datetime import timezone
13
+
14
+ # `datetime.UTC` chỉ có từ 3.11; `timezone.utc` là CÙNG một đối tượng và có ở
15
+ # mọi phiên bản. (ruff sẽ đòi đổi ngược lại nếu target-version trong ruff.toml
16
+ # bị nâng lên py311 — đừng nâng khi còn hỗ trợ 3.10.)
17
+ UTC = timezone.utc
18
+
19
+ # `asyncio.wait_for` ném `asyncio.TimeoutError`: từ 3.11 nó CHÍNH LÀ
20
+ # `TimeoutError` dựng sẵn, còn 3.10 thì là một lớp khác hẳn. Bắt bằng tên này
21
+ # thì đúng ở cả hai; bắt bằng `TimeoutError` trần sẽ trượt trên 3.10.
22
+ TimeoutErrors: tuple[type[BaseException], ...] = (asyncio.TimeoutError, TimeoutError)
23
+
24
+ if sys.version_info >= (3, 11):
25
+ from enum import StrEnum
26
+ else:
27
+ from enum import Enum
28
+
29
+ class StrEnum(str, Enum):
30
+ """Bản 3.10 của `enum.StrEnum`.
31
+
32
+ `__str__` phải trỏ về `str.__str__`: mặc định của Enum trả về
33
+ "Scope.REQUEST" chứ không phải "request", và đó là kiểu khác biệt chỉ lộ
34
+ ra khi ai đó nội suy giá trị vào log hay JSON.
35
+ """
36
+
37
+ __str__ = str.__str__
38
+
39
+ __all__ = ["UTC", "StrEnum", "TimeoutErrors"]