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
pymodular/cli/info.py ADDED
@@ -0,0 +1,62 @@
1
+ """`pym info` — đang cấu hình những gì, và thư viện nào đã cài.
2
+
3
+ Gộp sáu lệnh `*-info` cũ của Makefile vào một chỗ, vì câu hỏi thật sự luôn là "hiện tại
4
+ app của tôi đang nối vào đâu" chứ không phải "riêng RabbitMQ thế nào".
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib.util
10
+
11
+
12
+ def _co(ten: str) -> str:
13
+ return "có" if importlib.util.find_spec(ten) else "chưa cài"
14
+
15
+
16
+ def _dong(nhan: str, gia_tri: object) -> None:
17
+ print(f" {nhan:<16}{gia_tri}")
18
+
19
+
20
+ def info() -> int:
21
+ from pymodular import __version__
22
+ from pymodular.core.config import get_settings
23
+ from pymodular.infrastructure.mqtt.client import safe_url as mqtt_url
24
+ from pymodular.infrastructure.rabbitmq.broker import safe_url as amqp_url
25
+ from pymodular.infrastructure.redis.client import safe_url as redis_url
26
+
27
+ s = get_settings()
28
+
29
+ print(f"pymodular {__version__} · {type(s).__name__}")
30
+ print("\nỨng dụng")
31
+ _dong("name", s.name)
32
+ _dong("env", s.env)
33
+ _dong("host:port", f"{s.host}:{s.port}")
34
+
35
+ print("\nDatabase")
36
+ _dong("driver", s.db.driver)
37
+ _dong("dsn", s.db.resolved_dsn or "(bộ nhớ tạm — mất khi restart)")
38
+ _dong("schema_mode", s.db.schema_mode)
39
+ _dong("thư viện", f"sqlalchemy: {_co('sqlalchemy')} · asyncpg: {_co('asyncpg')} · "
40
+ f"aiosqlite: {_co('aiosqlite')} · motor: {_co('motor')}")
41
+
42
+ print("\nWebSocket")
43
+ _dong("adapter", s.ws.adapter + ("" if s.ws.adapter == "redis" else " (chỉ đúng với MỘT worker)"))
44
+ if s.ws.adapter == "redis":
45
+ _dong("redis_url", s.ws.redis_url)
46
+ _dong("nhịp tim", f"{s.ws.heartbeat_seconds}s / im lặng tối đa {s.ws.idle_timeout_seconds}s")
47
+
48
+ print("\nHạ tầng tuỳ chọn")
49
+ for nhan, bat, dia_chi, thu_vien in (
50
+ ("rabbitmq", s.rabbitmq.enabled, amqp_url(s.rabbitmq.url), "aio_pika"),
51
+ ("redis", s.redis.enabled, redis_url(s.redis.url), "redis"),
52
+ ("mqtt", s.mqtt.enabled, mqtt_url(s.mqtt.url), "aiomqtt"),
53
+ ("kafka", s.kafka.enabled, s.kafka.bootstrap_servers, "aiokafka"),
54
+ ):
55
+ trang_thai = f"bật {dia_chi}" if bat else "tắt"
56
+ _dong(nhan, f"{trang_thai:<45}thư viện: {_co(thu_vien)}")
57
+
58
+ if canh_bao := s.check_production_safety():
59
+ print("\nCẢNH BÁO cho môi trường prod")
60
+ for dong in canh_bao:
61
+ print(f" - {dong}")
62
+ return 0
@@ -0,0 +1,83 @@
1
+ """`pym install <thành-phần>` — cài thư viện của một thành phần rồi ghi .env.
2
+
3
+ Hai việc luôn đi cùng nhau nên gộp làm một: cài `aio-pika` mà quên ghi
4
+ `APP_RABBITMQ__*` thì lớp đó vẫn nằm im, còn ghi biến mà chưa cài thư viện thì
5
+ app báo `ComponentNotEnabledError` lúc khởi động.
6
+
7
+ Cài THẲNG các gói phụ thuộc chứ không chạy `pip install "fastapi-modular[x]"`. Lý do
8
+ rất thực tế: cách sau bắt pip đi tìm chính pymodular trên PyPI, nên hỏng ngay
9
+ khi bạn đang dùng bản cài từ file .whl, bản `pip install -e .`, hay bản chưa
10
+ phát hành.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import subprocess
16
+ import sys
17
+
18
+ # Nguồn sự thật là `[project.optional-dependencies]` trong pyproject.toml.
19
+ # `test_extras_khop_pyproject` giữ hai chỗ này không lệch nhau.
20
+ GOI: dict[str, list[str]] = {
21
+ "sqlite": ["sqlalchemy[asyncio]>=2.0.30,<3.0.0", "aiosqlite>=0.20.0", "alembic>=1.13.0"],
22
+ "postgres": ["sqlalchemy[asyncio]>=2.0.30,<3.0.0", "asyncpg>=0.29.0", "alembic>=1.13.0"],
23
+ "mongodb": ["motor>=3.6.0,<4.0.0"],
24
+ "rabbitmq": ["aio-pika>=9.4.0,<10.0.0"],
25
+ "redis": ["redis>=5.0.0,<7.0.0"],
26
+ "mqtt": ["aiomqtt>=2.0.0,<3.0.0"],
27
+ "kafka": ["aiokafka>=0.10.0,<0.13.0"],
28
+ "dev": ["pytest>=8.3.0", "pytest-asyncio>=0.24.0", "httpx>=0.27.0", "ruff>=0.6.0"],
29
+ }
30
+
31
+ # Thành phần -> khối .env tương ứng. `ws-redis` dùng chung thư viện với `redis`
32
+ # nhưng ghi khối cấu hình khác: một bên là lớp cache/pub-sub, một bên là adapter
33
+ # phát tin WebSocket xuyên worker.
34
+ KHOI_ENV: dict[str, str] = {
35
+ "sqlite": "sqlite",
36
+ "postgres": "postgres",
37
+ "mongodb": "mongodb",
38
+ "rabbitmq": "rabbitmq",
39
+ "redis": "redis",
40
+ "mqtt": "mqtt",
41
+ "kafka": "kafka",
42
+ "ws-redis": "ws-redis",
43
+ }
44
+
45
+ _ALIAS = {"mongo": "mongodb", "ws-redis": "redis", "postgresql": "postgres"}
46
+
47
+ THANH_PHAN = sorted({*GOI, *KHOI_ENV, "all"})
48
+
49
+
50
+ def install(ten: str, *, ghi_env: bool = True, env_file: object = None) -> int:
51
+ from pathlib import Path
52
+
53
+ if ten not in THANH_PHAN:
54
+ print(f"Không biết thành phần {ten!r}. Chọn một trong: {', '.join(THANH_PHAN)}")
55
+ return 1
56
+
57
+ goi_can = (
58
+ sorted({g for k, v in GOI.items() if k != "dev" for g in v})
59
+ if ten == "all"
60
+ else GOI[_ALIAS.get(ten, ten)]
61
+ )
62
+
63
+ print(f"Cài {len(goi_can)} gói cho '{ten}':")
64
+ for g in goi_can:
65
+ print(f" {g}")
66
+ ma = subprocess.call([sys.executable, "-m", "pip", "install", *goi_can])
67
+ if ma != 0:
68
+ print(
69
+ f"\npip thoát với mã {ma}. Cài tay:\n"
70
+ f" {sys.executable} -m pip install {' '.join(goi_can)}"
71
+ )
72
+ return ma
73
+
74
+ khoi = KHOI_ENV.get(ten)
75
+ if not ghi_env or khoi is None:
76
+ if khoi is None and ghi_env:
77
+ print(f"\n'{ten}' không có biến cấu hình riêng — không ghi .env.")
78
+ return 0
79
+
80
+ from pymodular.cli.configure_env import main as ghi
81
+
82
+ print()
83
+ return ghi(khoi, Path(env_file) if env_file else Path(".env"))
pymodular/cli/main.py ADDED
@@ -0,0 +1,247 @@
1
+ """Điểm vào của lệnh `pymodular`, gõ tắt là `pym`.
2
+
3
+ pym init dựng dự án ngay trong THƯ MỤC HIỆN TẠI
4
+ pym new duan-cua-toi dựng dự án trong một thư mục mới
5
+ pym dev chạy kèm autoreload
6
+ pym run --workers 4 chạy chế độ production
7
+ pym module alerts sinh module nghiệp vụ
8
+ pym install postgres cài thư viện của một thành phần + ghi .env
9
+ pym env postgres chỉ ghi biến cấu hình vào .env
10
+ pym info đang nối vào đâu, thư viện nào đã cài
11
+ pym migrate chạy migration (Alembic)
12
+ pym test / pym lint chạy test / soi lỗi tĩnh
13
+
14
+ Hai tên gọi cùng một chương trình; `pymodular` là tên đầy đủ cho script và tài
15
+ liệu, `pym` là để gõ hằng ngày.
16
+
17
+ Lệnh và tham số dạng danh sách đều rút gọn được, miễn là KHÔNG NHẬP NHẰNG:
18
+
19
+ pym mo alerts = pym module alerts
20
+ pym ins sq = pym install sqlite
21
+ pym d = pym dev
22
+
23
+ `pym m` thì báo lỗi kèm gợi ý, vì cả `module` lẫn `migrate` đều bắt đầu bằng "m"
24
+ — thà hỏi lại còn hơn đoán bừa rồi chạy nhầm lệnh.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import os
31
+ import sys
32
+ from pathlib import Path
33
+
34
+ from pymodular import __version__
35
+
36
+
37
+ def giai_nghia(tu: str, lua_chon: list[str], nhan: str) -> str:
38
+ """Đổi một từ viết tắt thành lựa chọn đầy đủ, nếu chỉ có đúng một khả năng.
39
+
40
+ Trả về nguyên `tu` khi nó đã là lựa chọn đầy đủ hoặc không khớp gì — để
41
+ argparse tự báo lỗi theo cách của nó. Chỉ ném lỗi khi NHẬP NHẰNG, vì đó là
42
+ trường hợp duy nhất mà im lặng đoán bừa sẽ chạy nhầm việc.
43
+ """
44
+ if tu in lua_chon:
45
+ return tu
46
+ khop = [c for c in lua_chon if c.startswith(tu)]
47
+ if len(khop) == 1:
48
+ return khop[0]
49
+ if len(khop) > 1:
50
+ raise SystemExit(
51
+ f"pym: {nhan} {tu!r} chưa rõ — khớp với {', '.join(sorted(khop))}. "
52
+ "Gõ thêm vài chữ cho rõ."
53
+ )
54
+ return tu
55
+
56
+
57
+ def _mo_rong_vietat(argv: list[str], lenh: list[str]) -> list[str]:
58
+ """Mở rộng từ viết tắt ở vị trí TÊN LỆNH, và ở tham số dạng danh sách."""
59
+ vi_tri = next((i for i, t in enumerate(argv) if not t.startswith("-")), None)
60
+ if vi_tri is None:
61
+ return argv
62
+
63
+ argv = list(argv)
64
+ argv[vi_tri] = giai_nghia(argv[vi_tri], lenh, "lệnh")
65
+
66
+ ke_tiep = next(
67
+ (i for i in range(vi_tri + 1, len(argv)) if not argv[i].startswith("-")), None
68
+ )
69
+ if ke_tiep is None:
70
+ return argv
71
+
72
+ if argv[vi_tri] in ("install", "env"):
73
+ from pymodular.cli.configure_env import BLOCKS
74
+ from pymodular.cli.install import THANH_PHAN
75
+
76
+ chon = THANH_PHAN if argv[vi_tri] == "install" else sorted(BLOCKS)
77
+ argv[ke_tiep] = giai_nghia(argv[ke_tiep], chon, "thành phần")
78
+ elif argv[vi_tri] == "migrate":
79
+ argv[ke_tiep] = giai_nghia(
80
+ argv[ke_tiep], ["up", "down", "history", "sql", "create"], "việc"
81
+ )
82
+ return argv
83
+
84
+
85
+ def main(argv: list[str] | None = None) -> int:
86
+ """Điểm vào thật. Bọc `_main` để `pym info | head` không văng traceback."""
87
+ try:
88
+ return _main(argv)
89
+ except BrokenPipeError:
90
+ # Người ta nối vào `head`/`less` rồi thoát sớm: đó là chuyện bình
91
+ # thường, không phải lỗi. Trỏ stdout vào /dev/null để lúc Python dọn
92
+ # dẹp không cố ghi thêm lần nữa.
93
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
94
+ return 0
95
+ except KeyboardInterrupt:
96
+ return 130
97
+
98
+
99
+ def _main(argv: list[str] | None = None) -> int:
100
+ parser = argparse.ArgumentParser(
101
+ prog="pym",
102
+ description="FastAPI theo kiến trúc module kiểu NestJS (gõ đầy đủ: pymodular)",
103
+ )
104
+ parser.add_argument("--version", action="version", version=f"pymodular {__version__}")
105
+ lenh = parser.add_subparsers(dest="lenh", required=True)
106
+
107
+ p_init = lenh.add_parser(
108
+ "init", help="dựng dự án ngay trong thư mục hiện tại (không tạo thêm cấp)"
109
+ )
110
+ p_init.add_argument("--name", help="tên dự án; mặc định lấy theo tên thư mục")
111
+ p_init.add_argument("--root", type=Path, default=Path("."), help="thư mục đích")
112
+
113
+ p_new = lenh.add_parser("new", help="dựng dự án trong một thư mục MỚI")
114
+ p_new.add_argument("ten", help="tên thư mục dự án")
115
+ p_new.add_argument("--root", type=Path, default=Path("."), help="tạo ở đâu (mặc định: .)")
116
+
117
+ p_mod = lenh.add_parser("module", help="sinh module nghiệp vụ")
118
+ p_mod.add_argument("ten", help="tên module, dạng số nhiều viết thường: alerts")
119
+ p_mod.add_argument("--entity", help="tên entity dạng số ít; mặc định đoán từ tên module")
120
+ p_mod.add_argument(
121
+ "--root", type=Path, default=Path("src/api"), help="thư mục chứa các module"
122
+ )
123
+ p_mod.add_argument("--gateway", action="store_true", help="tạo kèm gateway WebSocket")
124
+ p_mod.add_argument("--gateway-only", action="store_true")
125
+ p_mod.add_argument("--consumer", action="store_true", help="tạo kèm consumer RabbitMQ")
126
+ p_mod.add_argument("--consumer-only", action="store_true")
127
+
128
+ p_dev = lenh.add_parser("dev", help="chạy kèm autoreload")
129
+ p_run = lenh.add_parser("run", help="chạy chế độ production, nhiều worker")
130
+ for p_chay in (p_dev, p_run):
131
+ p_chay.add_argument("--app", default="src.main:app", help="điểm vào ASGI")
132
+ p_chay.add_argument("--host", help="mặc định lấy APP_HOST")
133
+ p_chay.add_argument("--port", type=int, help="mặc định lấy APP_PORT")
134
+ p_run.add_argument("--workers", type=int, default=4)
135
+
136
+ lenh.add_parser("info", help="cấu hình đang dùng và thư viện đã cài")
137
+
138
+ p_test = lenh.add_parser("test", help="chạy pytest")
139
+ p_test.add_argument("them", nargs="*", help="tham số truyền thẳng cho pytest")
140
+
141
+ p_lint = lenh.add_parser("lint", help="soi lỗi tĩnh bằng ruff")
142
+ p_lint.add_argument("--fix", action="store_true", help="tự sửa những lỗi sửa được")
143
+ p_lint.add_argument("duong_dan", nargs="*", default=["src"], help="mặc định: src")
144
+
145
+ p_mig = lenh.add_parser("migrate", help="chạy migration (Alembic)")
146
+ p_mig.add_argument(
147
+ "viec",
148
+ nargs="?",
149
+ default="up",
150
+ choices=["up", "down", "history", "sql", "create"],
151
+ help="up (mặc định) | down | history | sql | create",
152
+ )
153
+ p_mig.add_argument("-m", "--message", help="mô tả, dùng với `create`")
154
+
155
+ from pymodular.cli.install import THANH_PHAN
156
+
157
+ p_ins = lenh.add_parser(
158
+ "install", help="cài thư viện của một thành phần rồi ghi biến vào .env"
159
+ )
160
+ p_ins.add_argument("thanh_phan", choices=THANH_PHAN, metavar="thành-phần",
161
+ help=" | ".join(THANH_PHAN))
162
+ p_ins.add_argument("--no-env", action="store_true", help="chỉ cài, đừng đụng .env")
163
+ p_ins.add_argument("--file", type=Path, default=Path(".env"))
164
+
165
+ lenh.add_parser("clean", help="xoá cache và bản dựng (không đụng dữ liệu)")
166
+
167
+ p_build = lenh.add_parser("build", help="dựng wheel + sdist vào dist/")
168
+ p_build.add_argument("--no-clean", action="store_true", help="giữ dist/ cũ")
169
+
170
+ p_pub = lenh.add_parser("publish", help="đẩy gói lên PyPI")
171
+ p_pub.add_argument("--test", action="store_true", help="đẩy lên TestPyPI trước")
172
+
173
+ p_env = lenh.add_parser("env", help="ghi biến cấu hình của một thành phần vào .env")
174
+ p_env.add_argument("thanh_phan", help="sqlite | postgres | mongodb | redis | "
175
+ "rabbitmq | mqtt | kafka | ws-redis")
176
+ p_env.add_argument("--file", type=Path, default=Path(".env"))
177
+
178
+ args = parser.parse_args(
179
+ _mo_rong_vietat(list(argv if argv is not None else sys.argv[1:]), list(lenh.choices))
180
+ )
181
+
182
+ # Import muộn: `pymodular new` không cần kéo theo bộ sinh module, và
183
+ # `--version` thì không cần kéo theo gì cả.
184
+ if args.lenh == "init":
185
+ from pymodular.cli.new_project import init_du_an
186
+
187
+ return init_du_an(args.root, args.name)
188
+
189
+ if args.lenh == "new":
190
+ from pymodular.cli.new_project import tao_du_an
191
+
192
+ return tao_du_an(args.ten, args.root)
193
+
194
+ if args.lenh == "module":
195
+ from pymodular.cli.new_module import main as sinh_module
196
+
197
+ argv2 = [args.ten, "--root", str(args.root)]
198
+ if args.entity:
199
+ argv2 += ["--entity", args.entity]
200
+ for co, ten_co in (
201
+ (args.gateway, "--gateway"),
202
+ (args.gateway_only, "--gateway-only"),
203
+ (args.consumer, "--consumer"),
204
+ (args.consumer_only, "--consumer-only"),
205
+ ):
206
+ if co:
207
+ argv2.append(ten_co)
208
+ return sinh_module(argv2)
209
+
210
+ if args.lenh in ("dev", "run"):
211
+ from pymodular.cli.serve import serve
212
+
213
+ return serve(
214
+ target=args.app,
215
+ reload=args.lenh == "dev",
216
+ workers=getattr(args, "workers", None),
217
+ host=args.host,
218
+ port=args.port,
219
+ )
220
+
221
+ if args.lenh == "info":
222
+ from pymodular.cli.info import info
223
+
224
+ return info()
225
+
226
+ if args.lenh == "install":
227
+ from pymodular.cli.install import install
228
+
229
+ return install(args.thanh_phan, ghi_env=not args.no_env, env_file=args.file)
230
+
231
+ if args.lenh == "clean":
232
+ from pymodular.cli.clean import clean
233
+
234
+ return clean()
235
+
236
+ if args.lenh in ("test", "lint", "migrate", "build", "publish"):
237
+ from pymodular.cli.cong_cu import chay_cong_cu
238
+
239
+ return chay_cong_cu(args)
240
+
241
+ from pymodular.cli.configure_env import main as ghi_env
242
+
243
+ return ghi_env(args.thanh_phan, args.file)
244
+
245
+
246
+ if __name__ == "__main__":
247
+ raise SystemExit(main())