fastapi-modular 0.2.1__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. fastapi_modular-0.3.1/CHANGELOG.md +240 -0
  2. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/PKG-INFO +164 -12
  3. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/README.md +163 -11
  4. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/README.vi.md +159 -10
  5. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/README.md +7 -3
  6. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/architecture.md +49 -6
  7. fastapi_modular-0.3.1/docs/background.md +893 -0
  8. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/config.md +17 -1
  9. fastapi_modular-0.3.1/docs/database.md +2307 -0
  10. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/kafka.md +75 -29
  11. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/migrations.md +13 -1
  12. fastapi_modular-0.3.1/docs/mongodb.md +540 -0
  13. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/mqtt.md +59 -27
  14. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/operations.md +13 -0
  15. fastapi_modular-0.3.1/docs/providers.md +471 -0
  16. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/rabbitmq.md +338 -14
  17. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/redis.md +55 -18
  18. fastapi_modular-0.3.1/docs/rpc.md +487 -0
  19. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/__init__.py +32 -1
  20. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/clean.py +16 -16
  21. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/configure_env.py +152 -129
  22. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/info.py +21 -21
  23. fastapi_modular-0.3.1/fastapi_modular/cli/install.py +110 -0
  24. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/main.py +87 -74
  25. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/new_module.py +11 -9
  26. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/new_project.py +81 -50
  27. fastapi_modular-0.3.1/fastapi_modular/cli/new_provider.py +220 -0
  28. fastapi_modular-0.3.1/fastapi_modular/cli/requirements.py +162 -0
  29. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/serve.py +10 -10
  30. fastapi_modular-0.2.1/fastapi_modular/cli/cong_cu.py → fastapi_modular-0.3.1/fastapi_modular/cli/tools.py +34 -34
  31. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/config.py +166 -12
  32. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/container.py +72 -5
  33. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/controller.py +27 -2
  34. fastapi_modular-0.3.1/fastapi_modular/core/cron.py +227 -0
  35. fastapi_modular-0.3.1/fastapi_modular/core/events.py +518 -0
  36. fastapi_modular-0.3.1/fastapi_modular/core/jobs.py +377 -0
  37. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/lifespan.py +83 -4
  38. fastapi_modular-0.3.1/fastapi_modular/core/locks.py +215 -0
  39. fastapi_modular-0.3.1/fastapi_modular/core/providers.py +300 -0
  40. fastapi_modular-0.3.1/fastapi_modular/core/rpc.py +445 -0
  41. fastapi_modular-0.3.1/fastapi_modular/core/scheduler.py +525 -0
  42. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/websocket/gateway.py +2 -2
  43. fastapi_modular-0.3.1/fastapi_modular/core/workers.py +795 -0
  44. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/discovery.py +3 -3
  45. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/factory.py +19 -6
  46. fastapi_modular-0.3.1/fastapi_modular/infrastructure/database/__init__.py +53 -0
  47. fastapi_modular-0.3.1/fastapi_modular/infrastructure/database/base.py +469 -0
  48. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/database/circuit.py +10 -0
  49. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/database/factory.py +5 -2
  50. fastapi_modular-0.3.1/fastapi_modular/infrastructure/database/memory.py +416 -0
  51. fastapi_modular-0.3.1/fastapi_modular/infrastructure/database/mongo.py +468 -0
  52. fastapi_modular-0.3.1/fastapi_modular/infrastructure/database/query.py +1843 -0
  53. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/database/repository.py +64 -2
  54. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/database/sql.py +423 -14
  55. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/kafka/__init__.py +8 -0
  56. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/kafka/broker.py +165 -3
  57. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/kafka/consumers.py +19 -19
  58. fastapi_modular-0.3.1/fastapi_modular/infrastructure/kafka/responders.py +372 -0
  59. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/mqtt/__init__.py +8 -0
  60. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/mqtt/client.py +124 -28
  61. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/mqtt/consumers.py +9 -9
  62. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/mqtt/patterns.py +31 -31
  63. fastapi_modular-0.3.1/fastapi_modular/infrastructure/mqtt/responders.py +236 -0
  64. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/rabbitmq/__init__.py +8 -0
  65. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/rabbitmq/broker.py +408 -21
  66. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/rabbitmq/consumers.py +78 -25
  67. fastapi_modular-0.3.1/fastapi_modular/infrastructure/rabbitmq/patterns.py +159 -0
  68. fastapi_modular-0.3.1/fastapi_modular/infrastructure/rabbitmq/responders.py +398 -0
  69. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/redis/__init__.py +8 -0
  70. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/redis/client.py +195 -40
  71. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/redis/pubsub.py +23 -23
  72. fastapi_modular-0.3.1/fastapi_modular/infrastructure/redis/responders.py +280 -0
  73. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/devices/entities/device_model.py +2 -2
  74. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/health/health_controller.py +2 -2
  75. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/kafka_test/kafka_consumer.py +9 -9
  76. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/kafka_test/kafka_controller.py +2 -2
  77. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/mqtt_test/mqtt_controller.py +2 -2
  78. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/mqtt_test/mqtt_listener.py +5 -5
  79. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/rabbitmq_test/rabbitmq_consumer.py +5 -5
  80. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/rabbitmq_test/rabbitmq_controller.py +3 -3
  81. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/redis_test/redis_controller.py +11 -11
  82. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/redis_test/redis_listener.py +7 -7
  83. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/users/entities/user_model.py +2 -2
  84. fastapi_modular-0.3.1/tests/fixtures/cron_vectors.json +1802 -0
  85. fastapi_modular-0.3.1/tests/fixtures/nestjs_patterns.json +2605 -0
  86. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_cli.py +171 -86
  87. fastapi_modular-0.3.1/tests/test_configure_env.py +241 -0
  88. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_container.py +3 -3
  89. fastapi_modular-0.3.1/tests/test_controller_dong_bo.py +266 -0
  90. fastapi_modular-0.3.1/tests/test_cron.py +130 -0
  91. fastapi_modular-0.3.1/tests/test_enum_parity.py +121 -0
  92. fastapi_modular-0.3.1/tests/test_events.py +357 -0
  93. fastapi_modular-0.3.1/tests/test_foreign_keys.py +492 -0
  94. fastapi_modular-0.3.1/tests/test_ghi_du_lieu_tu_worker.py +254 -0
  95. fastapi_modular-0.3.1/tests/test_injection.py +171 -0
  96. fastapi_modular-0.3.1/tests/test_jobs.py +265 -0
  97. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_kafka.py +50 -12
  98. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_kafka_offline.py +72 -3
  99. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_lifespan_rieng.py +3 -3
  100. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_metrics.py +2 -2
  101. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_module_generator.py +1 -0
  102. fastapi_modular-0.3.1/tests/test_mongo_query.py +277 -0
  103. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_mqtt.py +76 -14
  104. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_mqtt_offline.py +50 -4
  105. fastapi_modular-0.3.1/tests/test_providers.py +482 -0
  106. fastapi_modular-0.3.1/tests/test_query_builder.py +1628 -0
  107. fastapi_modular-0.3.1/tests/test_rabbitmq.py +598 -0
  108. fastapi_modular-0.3.1/tests/test_rabbitmq_offline.py +715 -0
  109. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_redis.py +83 -14
  110. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_redis_offline.py +59 -2
  111. fastapi_modular-0.3.1/tests/test_requirements_ghi_nho.py +259 -0
  112. fastapi_modular-0.3.1/tests/test_rpc.py +208 -0
  113. fastapi_modular-0.3.1/tests/test_scheduler.py +316 -0
  114. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_schema_evolution.py +24 -4
  115. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_settings_mo_rong.py +6 -6
  116. fastapi_modular-0.3.1/tests/test_sqlite_pragmas.py +302 -0
  117. fastapi_modular-0.3.1/tests/test_transaction.py +279 -0
  118. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_tuong_thich_phien_ban.py +5 -5
  119. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_uniqueness.py +2 -2
  120. fastapi_modular-0.3.1/tests/test_workers.py +534 -0
  121. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_ws_adapter.py +9 -9
  122. fastapi_modular-0.2.1/CHANGELOG.md +0 -76
  123. fastapi_modular-0.2.1/docs/database.md +0 -703
  124. fastapi_modular-0.2.1/fastapi_modular/cli/install.py +0 -83
  125. fastapi_modular-0.2.1/fastapi_modular/infrastructure/database/__init__.py +0 -8
  126. fastapi_modular-0.2.1/fastapi_modular/infrastructure/database/base.py +0 -228
  127. fastapi_modular-0.2.1/fastapi_modular/infrastructure/database/memory.py +0 -112
  128. fastapi_modular-0.2.1/fastapi_modular/infrastructure/database/mongo.py +0 -186
  129. fastapi_modular-0.2.1/fastapi_modular/infrastructure/rabbitmq/patterns.py +0 -64
  130. fastapi_modular-0.2.1/tests/test_configure_env.py +0 -187
  131. fastapi_modular-0.2.1/tests/test_rabbitmq.py +0 -236
  132. fastapi_modular-0.2.1/tests/test_rabbitmq_offline.py +0 -273
  133. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/.gitignore +0 -0
  134. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/LICENSE +0 -0
  135. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/Makefile +0 -0
  136. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/alembic.ini +0 -0
  137. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/docs/websocket.md +0 -0
  138. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/cli/__init__.py +0 -0
  139. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/__init__.py +0 -0
  140. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/clock.py +0 -0
  141. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/compat.py +0 -0
  142. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/context.py +0 -0
  143. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/error_handlers.py +0 -0
  144. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/exceptions.py +0 -0
  145. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/guards.py +0 -0
  146. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/logging.py +0 -0
  147. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/metrics.py +0 -0
  148. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/schemas.py +0 -0
  149. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/websocket/__init__.py +0 -0
  150. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/websocket/adapter.py +0 -0
  151. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/websocket/namespace.py +0 -0
  152. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/websocket/protocol.py +0 -0
  153. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/websocket/server.py +0 -0
  154. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/core/websocket/socket.py +0 -0
  155. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/__init__.py +0 -0
  156. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/kafka/metrics.py +0 -0
  157. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/mqtt/metrics.py +0 -0
  158. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/rabbitmq/metrics.py +0 -0
  159. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/infrastructure/redis/metrics.py +0 -0
  160. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/middleware/__init__.py +0 -0
  161. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/middleware/request_context.py +0 -0
  162. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/fastapi_modular/py.typed +0 -0
  163. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/migrations/env.py +0 -0
  164. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/migrations/script.py.mako +0 -0
  165. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/migrations/versions/.gitkeep +0 -0
  166. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/migrations/versions/20260821_1020_tao_bang_users_va_devices.py +0 -0
  167. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/pyproject.toml +0 -0
  168. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/pytest.ini +0 -0
  169. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/ruff.toml +0 -0
  170. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/__init__.py +0 -0
  171. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/__init__.py +0 -0
  172. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/chat/__init__.py +0 -0
  173. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/chat/chat_controller.py +0 -0
  174. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/chat/chat_gateway.py +0 -0
  175. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/chat/dto/__init__.py +0 -0
  176. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/chat/dto/chat_dto.py +0 -0
  177. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/devices/__init__.py +0 -0
  178. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/devices/device_controller.py +0 -0
  179. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/devices/device_service.py +0 -0
  180. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/devices/dto/__init__.py +0 -0
  181. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/devices/dto/device_dto.py +0 -0
  182. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/devices/entities/__init__.py +0 -0
  183. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/health/__init__.py +0 -0
  184. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/kafka_test/__init__.py +0 -0
  185. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/metrics/__init__.py +0 -0
  186. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/metrics/metrics_controller.py +0 -0
  187. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/mqtt_test/__init__.py +0 -0
  188. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/rabbitmq_test/__init__.py +0 -0
  189. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/redis_test/__init__.py +0 -0
  190. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/users/__init__.py +0 -0
  191. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/users/dto/__init__.py +0 -0
  192. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/users/dto/user_dto.py +0 -0
  193. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/users/entities/__init__.py +0 -0
  194. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/users/user_controller.py +0 -0
  195. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/api/users/user_service.py +0 -0
  196. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/core/__init__.py +0 -0
  197. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/core/config.py +0 -0
  198. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/core/lifespan.py +0 -0
  199. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/src/main.py +0 -0
  200. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/conftest.py +0 -0
  201. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_circuit.py +0 -0
  202. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_cross_module.py +0 -0
  203. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_database.py +0 -0
  204. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_drivers.py +0 -0
  205. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_guards.py +0 -0
  206. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_ha_tang_that.py +0 -0
  207. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_health.py +0 -0
  208. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_resilience.py +0 -0
  209. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_schemas.py +0 -0
  210. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_users_api.py +0 -0
  211. {fastapi_modular-0.2.1 → fastapi_modular-0.3.1}/tests/test_websocket.py +0 -0
@@ -0,0 +1,240 @@
1
+ # Thay đổi
2
+
3
+ Theo [Keep a Changelog](https://keepachangelog.com/vi/1.1.0/); phiên bản theo
4
+ [SemVer](https://semver.org/lang/vi/).
5
+
6
+ ## [Chưa phát hành]
7
+
8
+ ## [0.3.1] — 2026-08-27
9
+
10
+ ### Sửa
11
+
12
+ - **Worker ghi database không được commit.** `contextvars` được sao chép khi tạo
13
+ Task/Thread, nên `@worker` sinh ra từ trong một HTTP request — hoặc từ
14
+ `@interval`/`@job`, vốn cũng mở request scope — thừa hưởng đúng store của
15
+ request đó. Mà `SqlUnitOfWork` là provider request-scoped: nó mở transaction
16
+ rồi chỉ commit ở `on_request_end`. Worker sống lâu hơn request nên transaction
17
+ ấy không bao giờ được commit.
18
+
19
+ Kiểu hỏng này im lặng đến khó chịu:
20
+
21
+ ```python
22
+ print("Deleted:", await repo.delete(row.id)) # -> True
23
+ ```
24
+
25
+ `True` là đúng — DELETE khớp một dòng, và câu SELECT ngay sau cũng thấy dữ
26
+ liệu mới vì cùng một connection. Chỉ có điều trên đĩa không đổi gì, và tắt app
27
+ là mất sạch.
28
+
29
+ Nay worker được cắt khỏi request scope thừa hưởng (`detach_request_scope`),
30
+ nên mỗi thao tác tự commit như docs vẫn mô tả. Gộp nhiều lệnh ghi thì bọc
31
+ `async with db.transaction():` — vẫn dùng được trong `ctx.run`.
32
+
33
+ Hệ quả có thể thấy: `container.resolve(<provider Scope.REQUEST>)` trong worker
34
+ giờ báo lỗi thay vì trả về một instance mồ côi. Đó là cố ý.
35
+
36
+ ## [0.3.0] — 2026-08-27
37
+
38
+ Bản này thêm **query builder** (JOIN, khoá ngoại, transaction, dữ liệu lồng
39
+ nhau), **việc chạy nền** (`@worker`, `@interval`, `@cron`, `@job`, `@on_event`),
40
+ **RPC gửi-rồi-chờ-trả-lời** cho cả bốn hạ tầng, và **provider cắm được**.
41
+
42
+ Nâng cấp từ 0.2.x cần sửa ba chỗ, xem mục "Thay đổi phá vỡ" ở cuối.
43
+
44
+ ### Thêm
45
+
46
+ - **Provider cắm được** — chọn bản hiện thực bằng TÊN lúc chạy, thứ mà container
47
+ (tra theo kiểu) không làm được: cổng thanh toán lấy từ cột trong đơn hàng, nhà
48
+ mạng SMS lấy từ cấu hình, hãng camera lấy từ bản ghi thiết bị.
49
+
50
+ Dùng đúng khuôn `Repository[User]` — service khai **năng lực** nó cần:
51
+
52
+ ```python
53
+ def __init__(self, payments: Providers[PaymentGateway]) -> None: ...
54
+
55
+ cong = self._payments.get(don.cong_thanh_toan) # -> PaymentGateway
56
+ ```
57
+
58
+ Thêm bản hiện thực mới = thả một file mang `@provider("tên")` vào
59
+ `src/providers/<họ>/`. Không sửa service, không sửa `main.py`.
60
+
61
+ `get()` trả 404 nếu không có tên, **501** nếu có tên nhưng thiếu năng lực —
62
+ Hik không mở được cửa thì đó không phải bug của server. `names()` chỉ liệt kê
63
+ provider làm được việc của sổ đó. Xem `docs/providers.md`, trong đó có mục
64
+ "Viết năng lực cho dễ bảo trì": khi nào tách `capabilities.py` thành package,
65
+ và ba nguyên tắc đặt tên / chia nhỏ interface.
66
+
67
+ - **`fam provider <họ> <tên>`** — sinh khung. Lần đầu dựng cả họ; lần sau đọc
68
+ `capabilities.py` rồi sinh sẵn stub đúng chữ ký các method cần viết.
69
+
70
+ - **`container.build(cls, key=..., scope=...)`** — dựng một lớp có nối phụ thuộc
71
+ mà KHÔNG đăng ký vào sổ toàn cục. Cần cho provider: sổ toàn cục tra theo tên
72
+ class, trong khi hai họ có quyền cùng có một `OryzaProvider`.
73
+
74
+ ### Thay đổi
75
+
76
+ - `fam p` giờ **nhập nhằng** giữa `publish` và `provider` nên `fam` hỏi lại. Viết
77
+ tắt mới: `fam pu` cho publish, `fam pr` cho provider.
78
+ - `create_app()` và `src/main.py` sinh sẵn gọi `register_providers()` trước khi
79
+ dựng route. Không có `src/providers/` thì bỏ qua, không lỗi.
80
+
81
+ ### Thêm — query builder
82
+
83
+ - **`repo.query()`** sinh SQL thật: `join` / `left_join` / `right_join` /
84
+ `outer_join`, `where` nối tiếp là AND còn `or_where` mở nhánh OR, `group_by` +
85
+ `having`, `limit` / `offset` / `distinct`. Xem câu sinh ra bằng `.sql()`.
86
+ - **Toán tử thường trên cột**: `Camera.score >= 0.9` thay cho `score__gte=0.9`,
87
+ nhờ `class Camera(Entity)`. Bảy toán tử không có ký hiệu (`like`, `ilike`,
88
+ `in_`, `not_in`, `is_null`, `is_not_null`, `between`) có cả ở dạng method của
89
+ builder — `query().like(...)` — để IDE gợi ý được.
90
+ - **Khoá ngoại**: `field(metadata=reference(Camera, on_delete="CASCADE"))`, đủ
91
+ bốn hành vi `CASCADE` / `SET NULL` / `SET DEFAULT` / `RESTRICT`. Áp bởi chính
92
+ database với SQL; khung tự áp cho `memory` và `mongodb` để ba backend cùng
93
+ kết quả.
94
+ - **Transaction**: `async with db.transaction() as tx:` — khối lồng nhau thành
95
+ SAVEPOINT, `await tx.rollback()` huỷ mà không phải ném lỗi. HTTP handler đã
96
+ nằm sẵn trong một transaction của cả request.
97
+ - **Dữ liệu lồng nhau**: `include(X)` khai X trả về những cột nào,
98
+ `nest_under(A, B, C)` khai thứ tự lồng từ NGOÀI vào TRONG. Mỗi mức đúng một
99
+ câu lệnh, không phải một câu cho mỗi dòng.
100
+ - **`select(...)`** gộp cả `fields=` / `exclude=` / `rename=` / `add=`.
101
+ - **MongoDB** chạy được phần lớn builder (`docs/mongodb.md` liệt kê cái không có:
102
+ JOIN, `group_by`, `distinct`, transaction).
103
+ - **Chặn injection ở tầng dùng chung**: giá trị mang toán tử (`{"$ne": ""}` —
104
+ qua được cửa đăng nhập trên Mongo), khoá `$where` chạy JavaScript, và tên cột
105
+ không có thật (SQL trước đây âm thầm bỏ điều kiện, trả về cả bảng).
106
+ - **Soi khoá ngoại lúc khởi động** (`db.foreign_keys_stale`): thêm
107
+ `reference(...)` vào entity đã chạy rồi thì database không biết — cascade
108
+ dừng giữa chừng, cháu ở lại thành mồ côi, không lỗi không cảnh báo.
109
+
110
+ ### Thêm — việc chạy nền
111
+
112
+ - `@worker` (vòng lặp sống mãi, N bản, mỗi bản một tham số), `@interval` /
113
+ `@cron` / `@timeout` (theo lịch), `@job` (hàng đợi trong tiến trình),
114
+ `@on_event` + `EventBus` (fanout trong tiến trình). Cả bốn nhận `thread=True`
115
+ cho hàm chặn, và `ctx: WorkerContext` để dừng đúng cách.
116
+ - Khoá `flock` / Redis để nhiều worker không cùng chạy một việc định kỳ.
117
+
118
+ ### Thêm — RPC và hạ tầng
119
+
120
+ - **`emit` / `send` + `@*_responder`** cho RabbitMQ, Redis, MQTT, Kafka — khuôn
121
+ tin tương thích `@nestjs/microservices`, đã chạy đối chứng hai chiều với
122
+ NestJS 11.2.1.
123
+ - RabbitMQ: đủ 5 kiểu exchange, 3 dạng hạn dùng (TTL), `emit_many`.
124
+ - SQLite: mặc định WAL + `synchronous=NORMAL` — đo được nhanh gấp 20 lần
125
+ (68 → 1.269 ghi/s) mà vẫn không hỏng file khi mất điện.
126
+
127
+ ### Thêm — CLI
128
+
129
+ - **`fam install` ghi nhớ thành phần vào `requirements.txt`**
130
+ (`fastapi-modular[redis,sqlite]>=0.3.0`), để người clone repo về chỉ cần
131
+ `pip install -r requirements.txt`. `fam init` sinh sẵn file này. Dự án dùng
132
+ `pyproject.toml` đã khai fastapi-modular thì sửa ngay dòng đó. `fam install
133
+ dev` đi vào `requirements-dev.txt`.
134
+ - `fam module` sinh entity kế thừa `Entity`.
135
+
136
+ ### Sửa
137
+
138
+ - **Controller viết `def` thường** nổ `TypeError: object dict can't be used in
139
+ 'await' expression`. Khung bọc mọi method thành endpoint async rồi await
140
+ thẳng, nên mất luôn luật của FastAPI: `def` phải chạy ở thread pool. Guard
141
+ đồng bộ cũng nổ y hệt. Đo lại sau khi sửa: 4 request chặn 0,3s xong trong
142
+ 0,31s.
143
+ - **`fam install` chạy trước `fam init`** chỉ ghi khối database vào `.env`,
144
+ thiếu sạch `APP_NAME` / `APP_ENV` / `APP_DEBUG` / `APP_HOST` / `APP_PORT` —
145
+ app chạy bằng toàn giá trị mặc định, im lặng.
146
+ - **Lọc bằng `Enum` thường** (không phải `StrEnum`) chạy trên `memory` nhưng nổ
147
+ trên sqlite lẫn mongo. Chiều ngược lại cũng lệch: lọc bằng chuỗi `.value` thì
148
+ SQL khớp còn memory trượt.
149
+ - **`nest_under` + sắp theo cột của một lớp**: sqlite ném lỗi, memory và mongo
150
+ lặng lẽ bỏ qua.
151
+ - **Lồng ba tầng từ repo trong cùng**: `include(Camera)` ném lỗi dù chuỗi
152
+ `nest_under` nối được qua bảng giữa.
153
+ - **`include(X, name=...)`** bị bỏ qua khi X nằm trong chuỗi `nest_under`.
154
+ - **Hai transaction `memory` đồng thời**: task rollback cuốn luôn bản ghi task
155
+ khác vừa commit.
156
+ - **MongoDB cho ghi con trỏ tới cha không tồn tại**, trong khi SQL và memory từ
157
+ chối 409.
158
+ - `Ctrl+C` không thoát được khi có worker đang chạy; `@worker(thread=True)` mượn
159
+ pool dùng chung của event loop nên làm treo cả tiến trình.
160
+
161
+ ### Thay đổi phá vỡ
162
+
163
+ | 0.2.x | 0.3.0 |
164
+ |---|---|
165
+ | `.order_by("score")` | `.order_by_desc("score")` / `.order_by_asc("score")` — chiều nằm trong TÊN HÀM |
166
+ | `.fields([...])` · `.exclude([...])` | `.select(fields=[...], exclude=[...])` |
167
+ | `fam p` | nhập nhằng — `fam pu` (publish) hoặc `fam pr` (provider) |
168
+
169
+ Định danh trong thư viện đổi hết sang tiếng Anh; nếu bạn import hàm `_private`
170
+ nào của khung thì kiểm lại tên. API công khai không đổi.
171
+
172
+ ## [0.2.1] — 2026-08-22
173
+
174
+ ### Thay đổi
175
+
176
+ - Tác giả và chủ bản quyền: Oryza <developer@oryza.vn> -> quanglinh
177
+ <hackcoquanglinh2000@gmail.com>, ở cả `pyproject.toml` lẫn `LICENSE`.
178
+ - README tách làm hai bản song ngữ: `README.md` (tiếng Anh, là bản hiện trên
179
+ PyPI) và `README.vi.md` (tiếng Việt). Hai bản giữ cùng thứ tự mục.
180
+ - Metadata PyPI viết lại cho tìm kiếm: summary sang tiếng Anh, keywords từ 9 lên
181
+ 39 từ, thêm 7 classifier.
182
+
183
+ ### Tài liệu
184
+
185
+ - Viết lại `docs/websocket.md` theo hướng làm-theo thay vì tra-cứu: đưa "bốn
186
+ việc client bắt buộc phải làm" lên đầu trang, thêm client tối thiểu 30 dòng
187
+ chạy được ngay, giải thích cơ chế nhịp tim bằng sơ đồ thời gian, và thêm mục
188
+ tra sự cố theo triệu chứng.
189
+
190
+ ## [0.2.0] — 2026-08-22
191
+
192
+ **Đổi tên toàn bộ.** Tên `pymodular` bị PyPI từ chối vì trùng với project
193
+ `py-modular` đã có, nên cả dự án đổi sang `fastapi-modular` cho thống nhất từ
194
+ PyPI, GitHub, thư mục nguồn cho tới tên lệnh.
195
+
196
+ ### Phá vỡ
197
+
198
+ - Gói trên PyPI: `pymodular` -> **`fastapi-modular`**.
199
+ - Tên import: `import pymodular` -> **`import fastapi_modular`** (gạch dưới,
200
+ vì gạch ngang không hợp lệ trong tên module Python).
201
+ - Lệnh CLI: `pymodular` / `pym` -> **`fastapi-modular` / `fam`**.
202
+ - Thư mục nguồn `pymodular/` -> `fastapi_modular/`.
203
+ - `APP_NAME` mặc định: `pymodular` -> `fastapi-modular`.
204
+ - `APP_KAFKA__CLIENT_ID` mặc định: `pymodular` -> `fastapi-modular`.
205
+
206
+ Nâng cấp từ 0.1.0: gỡ gói cũ (`pip uninstall pymodular`), cài
207
+ `pip install fastapi-modular`, rồi đổi mọi `pymodular` trong import thành
208
+ `fastapi_modular` và mọi lệnh `pym` thành `fam`.
209
+
210
+ ### Sửa
211
+
212
+ - `fam lint` không tham số trỏ vào thư mục `app` không tồn tại nên lỗi ngay;
213
+ mặc định đổi thành `src`, đúng thứ `fam init` sinh ra.
214
+ - `fam migrate-create` trong docs/migrations.md không phải lệnh có thật; lệnh
215
+ đúng là `fam migrate create`.
216
+ - Link trong README đổi sang URL tuyệt đối: README là trang hiển thị trên PyPI,
217
+ ở đó link tương đối `docs/...` phân giải sai và trả 404.
218
+ - Job đóng gói trong CI không bao giờ xanh được: import sai đường dẫn
219
+ (`src.api.main`) và liệt kê route bằng `r.path`, hỏng từ FastAPI 0.141.
220
+
221
+ ## [0.1.0] — 2026-08-21
222
+
223
+ Bản đầu tiên. Cần **Python 3.10 trở lên**.
224
+
225
+ ### Có gì
226
+
227
+ - **Kiến trúc module kiểu NestJS**: DI container (`@injectable`, `Lazy[...]`,
228
+ `Scope.REQUEST`), controller dạng class (`@controller`, `@get`/`@post`/...),
229
+ tự quét module — thêm thư mục là có route, không phải đăng ký ở đâu cả.
230
+ - **Repository dùng chung cho 4 backend**: memory, SQLite, PostgreSQL, MongoDB.
231
+ Đổi backend không phải sửa service. Kèm circuit breaker và hạn thời gian.
232
+ - **WebSocket**: `@gateway` / `@subscribe`, phòng, gửi thẳng tới một người,
233
+ nhịp tim, giới hạn tần suất, adapter Redis để phát tin xuyên worker.
234
+ - **Bốn lớp hạ tầng tuỳ chọn, cùng một khuôn**: RabbitMQ, Redis, MQTT, Kafka.
235
+ Mặc định TẮT; thư viện chỉ được import khi bật. Tất cả tự nối lại.
236
+ - **CLI** `fastapi-modular`, gõ tắt là `fam`: `init` (dựng dự án ngay trong thư mục
237
+ hiện tại) · `new` · `dev` / `run` · `module` · `env` · `info` · `migrate` ·
238
+ `test` / `lint`. `init` không bao giờ ghi đè file đã có.
239
+ - **Tài liệu tiếng Việt** trong `docs/`, viết theo lối tra cứu: mỗi hàm nói rõ
240
+ truyền gì, không truyền thì mặc định là gì.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fastapi-modular
3
- Version: 0.2.1
3
+ Version: 0.3.1
4
4
  Summary: NestJS-style modular architecture for FastAPI: DI container, class-based controllers, auto-discovered modules, WebSocket gateway, RabbitMQ/Redis/MQTT/Kafka
5
5
  Project-URL: Homepage, https://github.com/quanglinh2909/fastapi-modular
6
6
  Project-URL: Documentation, https://github.com/quanglinh2909/fastapi-modular/tree/main/docs
@@ -97,14 +97,33 @@ public API, and this README, are in English. Start with
97
97
  | `@Module()` + module scanning | a directory under `src/api/`, auto-scanned |
98
98
  | `@Controller('users')` | `@controller(prefix="/users", tags=["users"])` |
99
99
  | `@Get()` `@Post()` `@Patch()` `@Delete()` | `@get()` `@post()` `@patch()` `@delete()` |
100
+ | sync handler runs on the main thread (Nest) | `def` handler runs in a thread pool, `async def` on the event loop — FastAPI's own rule |
100
101
  | `@Injectable()` | `@injectable` |
101
102
  | `@Injectable({scope: Scope.REQUEST})` | `@injectable(scope=Scope.REQUEST)` |
102
103
  | `forwardRef(() => X)` | `Lazy[X]` |
103
104
  | `@InjectRepository(X) repo: Repository<X>` | `repo: Repository[X]` |
105
+ | `@Transaction()` / `queryRunner.startTransaction()` | `async with db.transaction():` — nested blocks become SAVEPOINTs |
106
+ | `queryRunner.rollbackTransaction()` | automatic on exception; `await tx.rollback()` to bail out without raising |
107
+ | `repo.createQueryBuilder()` (TypeORM) | `repo.query().join(X).where(Event.score >= …)` — real SQL, `.sql()` to see it |
108
+ | `Repository.find({where: {score: MoreThan(…)}})` (TypeORM) | `class Event(Entity)` then `.where(Event.score >= …)`, or `.where(score__gte=…)` |
109
+ | `.groupBy().having()` (TypeORM) | `.group_by(Event.camera_id).select(n=count()).having(count() > 5)` |
110
+ | `.leftJoin()` / `.orWhere()` (TypeORM) | `.left_join(X)` / `.or_where(…)` — one method per join kind |
111
+ | `.orderBy('x', 'DESC')` (TypeORM) | `.order_by_desc("x")` — the direction is in the method name |
112
+ | `Like()` / `In()` / `IsNull()` (TypeORM) | `.like(X.name, "a%")` · `.in_(X.zone, [...])` · `.is_null(X.ip)` — right on the builder |
113
+ | `select([...])` / `AS` (TypeORM) | `.select(fields=…, exclude=…, rename={"new": "col"})` — same names on `include` |
114
+ | `addSelect()` (TypeORM) | `.select(add={"cam_name": Camera.name})` — keep every column, add one |
115
+ | `find({relations: {events: true}})` (TypeORM) | `.include(Event)` — nested rows, one extra query, not N+1 |
116
+ | `relations: {camera: {logs: {items: true}}}` (TypeORM) | `.nest_under(Camera, CameraLog, ItemLog)` — one query per level |
117
+ | *(no TypeORM equivalent)* | `.nest_under(Camera)` — filter on events, get cameras back with them nested |
118
+ | `@ManyToOne(…, {onDelete: 'CASCADE'})` (TypeORM) | `field(metadata=reference(Camera, on_delete="CASCADE"))` — a real FK in the database |
104
119
  | `@UseGuards()` | `guards=[...]` on the controller or a single route |
105
120
  | `@WebSocketGateway()` | `@gateway(path="/ws/…")` |
106
121
  | `@SubscribeMessage('x')` | `@subscribe("x")` |
107
122
  | `@EventPattern('x')` (RabbitMQ) | `@rabbitmq_subscriber("events", "x", queue="…")` |
123
+ | `@MessagePattern('x')` | `@rabbitmq_responder("x", queue="…")` — the return value is sent back |
124
+ | `@Interval()` / `@Cron()` / `@Timeout()` | `@interval(seconds=5)` / `@cron("0 3 * * *")` / `@timeout(seconds=10)` |
125
+ | `@OnEvent('x')` + `EventEmitter2` | `@on_event("x")` + `EventBus.emit()` — in-process fanout |
126
+ | `client.emit(p, d)` / `client.send(p, d)` | `broker.emit(p, d, queue=…)` / `await broker.send(p, d, queue=…)` |
108
127
  | `CacheModule` / `CACHE_MANAGER` | `RedisClient.cached(key, factory, ttl=…)` |
109
128
  | socket.io Redis adapter | `APP_WS__ADAPTER=redis` |
110
129
 
@@ -139,8 +158,24 @@ fam install rabbitmq # or redis, mqtt, kafka
139
158
  fam install all # everything above
140
159
  ```
141
160
 
142
- `fam install` both installs the libraries and writes the matching variables into
143
- `.env`. Plain pip works too: `pip install "fastapi-modular[sqlite,rabbitmq]"`.
161
+ `fam install` does three things: installs the libraries, writes the matching
162
+ variables into `.env`, and **records the component in `requirements.txt`** so a
163
+ teammate who clones the repo only needs `pip install -r requirements.txt` — the
164
+ same job `package.json` does for `npm i`.
165
+
166
+ ```
167
+ # requirements.txt, after `fam install sqlite` and `fam install redis`
168
+ fastapi-modular[redis,sqlite]>=0.3.1
169
+ ```
170
+
171
+ It records the extras, not the individual packages: the version ranges of
172
+ `sqlalchemy`, `motor` and friends belong to fastapi-modular and change per
173
+ release, so a flattened snapshot would go stale silently. If the project uses
174
+ `pyproject.toml` and already lists fastapi-modular there, that line is updated
175
+ instead and no `requirements.txt` is created. `fam install dev` goes to
176
+ `requirements-dev.txt` — production should not have to install pytest.
177
+
178
+ Plain pip works too: `pip install "fastapi-modular[sqlite,rabbitmq]"`.
144
179
 
145
180
  ## Commands
146
181
 
@@ -166,9 +201,10 @@ fam: lệnh 'm' chưa rõ — khớp với migrate, module. Gõ thêm vài chữ
166
201
  | `fam module <name> --gateway` | | plus a WebSocket gateway (`--consumer` for RabbitMQ) |
167
202
  | `fam module <name> --gateway-only` | | add a gateway to an **existing** module (`--consumer-only` for RabbitMQ) |
168
203
  | `fam module <name> --entity <N>` | | set the entity class name; guessed from the module name otherwise |
204
+ | `fam provider <family> <name>` | `fam pr` | generate a pluggable provider: capability interfaces + implementation stub |
169
205
  | `fam env <component>` | `fam e` | only write config variables into `.env` (installs nothing) |
170
206
  | `fam clean` | `fam c` | remove caches and build output (leaves `data/` alone) |
171
- | `fam build` · `fam publish [--test]` | `fam b` · `fam p` | build wheel/sdist · upload to PyPI |
207
+ | `fam build` · `fam publish [--test]` | `fam b` · `fam pu` | build wheel/sdist · upload to PyPI |
172
208
  | `fam info` | `fam inf` | what it connects to, what is installed, production config warnings |
173
209
  | `fam migrate [up\|down\|history\|sql\|create]` | `fam mi` | Alembic |
174
210
  | `fam test` · `fam lint [--fix]` | `fam t` · `fam l` | pytest · ruff. Bare `fam lint` checks `src`; pass paths to check elsewhere |
@@ -177,7 +213,7 @@ fam: lệnh 'm' chưa rõ — khớp với migrate, module. Gõ thêm vài chữ
177
213
  | `fam install postgres` | `fam ins p` | PostgreSQL |
178
214
  | `fam install mongodb` | `fam ins mo` | MongoDB |
179
215
  | **Queues** | | *installs libraries **then** writes `.env`* |
180
- | `fam install rabbitmq` | `fam ins ra` | durable queues, retry + DLQ |
216
+ | `fam install rabbitmq` | `fam ins ra` | 5 exchange types, durable queues, TTL, retry + DLQ |
181
217
  | `fam install redis` | `fam ins re` | cache, atomic counters, pub/sub |
182
218
  | `fam install mqtt` | `fam ins mq` | IoT devices |
183
219
  | `fam install kafka` | `fam ins k` | replayable event log |
@@ -214,8 +250,7 @@ fam env sqlite # write .env only, install nothing
214
250
  fam info # what it is connected to right now
215
251
  ```
216
252
 
217
- Details:
218
- [docs/database.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/database.md).
253
+ Details: [docs/database.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/database.md) (SQL) · [docs/mongodb.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/mongodb.md) (MongoDB).
219
254
 
220
255
  ## Your own configuration
221
256
 
@@ -306,12 +341,125 @@ async def send_mail(self, payload: AlertCreated) -> None: ...
306
341
  @rabbitmq_subscriber("events", "alert.created", queue="alert-mailer",
307
342
  max_retries=3, dead_letter=True)
308
343
  async def send_mail(self, payload: AlertCreated) -> None: ...
344
+
345
+ # All 5 exchange types: topic (default), direct, fanout, headers, default
346
+ @rabbitmq_subscriber("cache-events", queue=f"drop-cache-{HOSTNAME}", exchange_type="fanout")
347
+ # hostname in the queue name -> every worker gets a copy, instead of taking turns
348
+ async def drop_cache(self, payload: dict) -> None: ...
349
+
350
+ # Time to live: per message (ttl), per queue (message_ttl), for the queue itself
351
+ await self._mq.publish("events", "car.position", {"lat": 21.0}, ttl=5)
309
352
  ```
310
353
 
311
354
  Not installed and not enabled means it behaves as if it never existed. If the
312
355
  broker goes down the app keeps serving and reconnects on its own. Details:
313
356
  [docs/rabbitmq.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/rabbitmq.md).
314
357
 
358
+ ## Background work
359
+
360
+ Four different things, none of which needs any infrastructure:
361
+
362
+ ```python
363
+ # on a SCHEDULE — @nestjs/schedule
364
+ @interval(seconds=5)
365
+ async def update_cameras(self) -> None: ...
366
+
367
+ @cron("0 3 * * *", timezone="Asia/Ho_Chi_Minh") # defaults to UTC!
368
+ async def clean_logs(self) -> None: ...
369
+
370
+ # on DEMAND — an in-process asyncio.Queue, processed in order
371
+ @job("detect", thread=True) # thread: runs in a thread, for YOLO
372
+ def detect(self, payload: dict, ctx: WorkerContext) -> None:
373
+ ctx.run(self._db.save(...)) # write to the DB from inside the thread
374
+
375
+ await self._jobs.submit("detect", {"path": p}) # returns immediately
376
+
377
+ # a LONG-RUNNING LOOP — N instances, one per key
378
+ @worker("camera")
379
+ async def watch(self, data: dict, ctx: WorkerContext) -> None:
380
+ cap = await ctx.blocking(cv2.VideoCapture, data["ip"]) # setup, OUTSIDE the loop
381
+ while ctx.running:
382
+ frame = await ctx.blocking(cap.read) # blocking call -> a thread
383
+ await self._db.save(...) # plain await
384
+
385
+ for camera in cameras:
386
+ await service.watch(camera.id, {"ip": camera.ip}) # key + data at call time
387
+
388
+ await self.watch.stop(camera.id) # stops ONE instance, waits for its cleanup
389
+
390
+ # FANOUT inside the process — one event, N listeners, in PARALLEL
391
+ @on_event("order.paid") # also "order.*" / "camera.#"
392
+ async def send_receipt(self, data: dict) -> None: ...
393
+
394
+ @on_event("order.paid") # a second listener is normal here
395
+ async def update_stats(self, data: dict) -> None: ...
396
+
397
+ await self._events.emit("order.paid", {"id": id}) # waits for all of them
398
+ self._events.dispatch("order.paid", {"id": id}) # returns immediately
399
+ ```
400
+
401
+ All five decorators come in two shapes: `async def` (the default) and
402
+ `thread=True` for bodies that are all blocking calls. `ctx` is optional — take
403
+ it when you need `ctx.running` to leave a loop, `ctx.blocking(...)` to call
404
+ blocking code, or `ctx.run(...)` to write to the DB from inside a thread.
405
+
406
+ `@on_event` covers what `@job` cannot: `@job` is one name, **one** handler,
407
+ processed in order — a work queue. `@on_event` is one event, **many** handlers,
408
+ running in parallel — nobody owns the work, and the emitter doesn't know who is
409
+ listening. One listener raising doesn't stop the others. It is `fanout` /
410
+ `EventEmitter`, but in-process only: with `fam run --workers 4` an event does
411
+ not cross to the other three processes.
412
+
413
+ `@worker` covers what `@interval` and `@job` cannot: a setup phase **before**
414
+ the loop (open the camera, load the model) and a body that runs until you stop
415
+ it. Crashes restart with backoff; calling it again with the same `key` returns
416
+ the running instance instead of opening a second stream.
417
+
418
+ `stop()` waits for the loop's `finally:` to finish, so anything you write after
419
+ it runs with the camera already closed — put resource cleanup in `finally:` and
420
+ business cleanup after the call.
421
+
422
+ Write `while ctx.running:`, not `while True:` — a loop that never checks makes
423
+ Ctrl+C look dead for the whole shutdown timeout. The framework says so at
424
+ startup (`worker.endless_loop`) rather than letting you find out at 2am.
425
+
426
+ `fam run` starts 4 workers, so a hand-written `while True: sleep(5)` runs
427
+ **four times**. `single=True` (the default) locks it down: measured 5 runs
428
+ across 1 process, versus 20 runs across 4 with the lock off. The lock is
429
+ `flock` (one machine) or Redis (many), picked automatically.
430
+
431
+ The `@job` queue lives in RAM — **shutdown loses whatever hasn't run**, and the
432
+ framework logs that count instead of hiding it. Work that must not be lost
433
+ belongs in `@rabbitmq_subscriber`. Details:
434
+ [docs/background.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/background.md).
435
+
436
+ ## Request/response, NestJS-compatible
437
+
438
+ `publish`/`emit` fire and forget. `send` waits for an answer — the NestJS
439
+ `client.send()` / `@MessagePattern()` pair, same wire format:
440
+
441
+ ```python
442
+ # the side that answers — a normal service that happens to `return`
443
+ @rabbitmq_responder("sum", queue="math")
444
+ async def add(self, data: list[int]) -> int:
445
+ return sum(data)
446
+
447
+ # the side that calls
448
+ total = await self._mq.send("sum", [1, 2, 3, 4], queue="math") # -> 10
449
+ ```
450
+
451
+ Available on **all four**: `@rabbitmq_responder`, `@redis_responder`,
452
+ `@mqtt_responder`, `@kafka_responder`.
453
+
454
+ The packet layout is taken from `@nestjs/microservices` source, so a NestJS
455
+ microservice and a service built on this library talk to each other with no
456
+ translation layer — verified both ways, on all four transports, against NestJS
457
+ 11.2.1. Object patterns (`{"cmd": "sum"}`) included, key ordering and all.
458
+
459
+ `send` turns a queue into a network call, which brings back everything queues
460
+ exist to avoid — [docs/rpc.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/rpc.md)
461
+ says when not to use it.
462
+
315
463
  ## Redis, MQTT, Kafka (also optional)
316
464
 
317
465
  Same shape as RabbitMQ: one package under `infrastructure/`, one `APP_<NAME>__*`
@@ -354,8 +502,8 @@ Guards, circuit breaker, metrics and tracing:
354
502
  fastapi_modular/ THE LIBRARY — what gets packaged and installed
355
503
  core/ DI, controllers, config, WebSocket, guards, metrics
356
504
  infrastructure/ database, rabbitmq, redis, mqtt, kafka (one package each)
357
- cli/ init · new · module · dev · run · install · env · info
358
- migrate · test · lint · clean · build · publish
505
+ cli/ init · new · module · provider · dev · run · install · env
506
+ info · migrate · test · lint · clean · build · publish
359
507
  factory.py create_app()
360
508
  discovery.py scans the application package and builds routers
361
509
  src/ SAMPLE APPLICATION — not shipped in the package; delete freely
@@ -363,7 +511,7 @@ src/ SAMPLE APPLICATION — not shipped in the package; delete fr
363
511
  core/config.py AppSettings: subclass Settings to add your own .env variables
364
512
  core/lifespan.py application-specific startup / shutdown work
365
513
  api/ business modules; every subdirectory is one module
366
- tests/ 341 tests that need no infrastructure, 40 more when servers exist
514
+ tests/ 1101 tests that need no infrastructure, 292 more with real drivers/servers
367
515
  docs/ reference documentation (Vietnamese)
368
516
  ```
369
517
 
@@ -402,11 +550,15 @@ Written in Vietnamese, organised for reference rather than reading front to back
402
550
 
403
551
  - [docs/architecture.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/architecture.md) — module layout, DI, the NestJS comparison
404
552
  - [docs/config.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/config.md) — Settings, precedence, adding your own variables
405
- - [docs/database.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/database.md) — memory / SQLite / PostgreSQL / MongoDB
553
+ - [docs/database.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/database.md) — SQL: memory / SQLite / PostgreSQL entities, foreign keys, query builder, transactions
554
+ - [docs/mongodb.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/mongodb.md) — MongoDB: queries, nested data, and what is not there (no JOIN, no transactions)
406
555
  - [docs/migrations.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/migrations.md) — Alembic: generate, run, roll back
407
556
  - [docs/websocket.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/websocket.md) — WebSocket gateway, rooms, Postman, Next.js
408
- - [docs/rabbitmq.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/rabbitmq.md) — exchanges, topics, background consumers, `.retry` / `.dlq`
557
+ - [docs/rabbitmq.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/rabbitmq.md) — all 5 exchange types, TTL, background consumers, `.retry` / `.dlq`
558
+ - [docs/background.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/background.md) — scheduled work (`@interval`/`@cron`/`@timeout`), an in-process job queue (`@job`), long-running loops (`@worker`) and in-process fanout (`@on_event`)
559
+ - [docs/rpc.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/rpc.md) — `emit` / `send` / `@rabbitmq_responder`, NestJS-compatible wire format
409
560
  - [docs/redis.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/redis.md) — cache, atomic counters, pub/sub
410
561
  - [docs/mqtt.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/mqtt.md) — QoS, retain, `+` and `#` topic matching
411
562
  - [docs/kafka.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/kafka.md) — consumer groups, partitions, `.dlt`
563
+ - [docs/providers.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/providers.md) — pluggable providers: pick an implementation by name at runtime
412
564
  - [docs/operations.md](https://github.com/quanglinh2909/fastapi-modular/blob/main/docs/operations.md) — guards, circuit breaker, metrics, tracing