zrb 1.0.0b7__py3-none-any.whl → 1.0.0b9__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 (40) hide show
  1. zrb/__main__.py +3 -0
  2. zrb/builtin/git.py +15 -15
  3. zrb/builtin/git_subtree.py +6 -6
  4. zrb/builtin/project/add/fastapp/fastapp_task.py +1 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +1 -0
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_service.py +5 -5
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +1 -0
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +8 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +9 -2
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +100 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +6 -86
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +27 -11
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +32 -27
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +15 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +22 -5
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +21 -0
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +103 -61
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +3 -4
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +15 -14
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +4 -4
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +24 -5
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +14 -12
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +130 -96
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +28 -11
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +220 -13
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +30 -2
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +27 -2
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +2 -1
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +13 -12
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +55 -12
  32. zrb/task/cmd_task.py +4 -7
  33. zrb/util/cmd/command.py +41 -50
  34. zrb/util/git.py +18 -18
  35. zrb/util/git_subtree.py +6 -6
  36. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/METADATA +2 -1
  37. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/RECORD +39 -39
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +0 -48
  39. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/WHEEL +0 -0
  40. {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/entry_points.txt +0 -0
zrb/__main__.py CHANGED
@@ -16,8 +16,11 @@ def serve_cli():
16
16
  cli.run(sys.argv[1:])
17
17
  except KeyboardInterrupt:
18
18
  print(stylize_warning("\nStopped"), file=sys.stderr)
19
+ sys.exit(1)
19
20
  except RuntimeError as e:
20
21
  if f"{e}".lower() != "event loop is closed":
21
22
  raise e
23
+ sys.exit(1)
22
24
  except NodeNotFoundError as e:
23
25
  print(stylize_error(f"{e}"), file=sys.stderr)
26
+ sys.exit(1)
zrb/builtin/git.py CHANGED
@@ -57,9 +57,9 @@ from zrb.util.git import (
57
57
  )
58
58
  async def get_git_diff(ctx: AnyContext):
59
59
  ctx.print(stylize_faint("Get directory"))
60
- repo_dir = await get_repo_dir(log_method=ctx.print)
60
+ repo_dir = await get_repo_dir(print_method=ctx.print)
61
61
  diff = await get_diff(
62
- repo_dir, ctx.input.source, ctx.input.current, log_method=ctx.print
62
+ repo_dir, ctx.input.source, ctx.input.current, print_method=ctx.print
63
63
  )
64
64
  result = []
65
65
  decorated = []
@@ -88,17 +88,17 @@ async def get_git_diff(ctx: AnyContext):
88
88
  )
89
89
  async def prune_local_branches(ctx: AnyContext):
90
90
  ctx.print(stylize_faint("Get directory"))
91
- repo_dir = await get_repo_dir(log_method=ctx.print)
91
+ repo_dir = await get_repo_dir(print_method=ctx.print)
92
92
  ctx.print(stylize_faint("Get existing branches"))
93
- branches = await get_branches(repo_dir, log_method=ctx.print)
93
+ branches = await get_branches(repo_dir, print_method=ctx.print)
94
94
  ctx.print(stylize_faint("Get current branch"))
95
- current_branch = await get_current_branch(repo_dir, log_method=ctx.print)
95
+ current_branch = await get_current_branch(repo_dir, print_method=ctx.print)
96
96
  for branch in branches:
97
97
  if branch == current_branch or branch == "main" or branch == "master":
98
98
  continue
99
99
  ctx.print(stylize_faint(f"Removing local branch: {branch}"))
100
100
  try:
101
- await delete_branch(repo_dir, branch, log_method=ctx.print)
101
+ await delete_branch(repo_dir, branch, print_method=ctx.print)
102
102
  except Exception as e:
103
103
  ctx.log_error(e)
104
104
 
@@ -117,11 +117,11 @@ async def prune_local_branches(ctx: AnyContext):
117
117
  )
118
118
  async def git_commit(ctx: AnyContext):
119
119
  ctx.print(stylize_faint("Get directory"))
120
- repo_dir = await get_repo_dir(log_method=ctx.print)
120
+ repo_dir = await get_repo_dir(print_method=ctx.print)
121
121
  ctx.print(stylize_faint("Add changes to staging"))
122
- await add(repo_dir, log_method=ctx.print)
122
+ await add(repo_dir, print_method=ctx.print)
123
123
  ctx.print(stylize_faint("Commit changes"))
124
- await commit(repo_dir, ctx.input.message, log_method=ctx.print)
124
+ await commit(repo_dir, ctx.input.message, print_method=ctx.print)
125
125
 
126
126
 
127
127
  @make_task(
@@ -139,12 +139,12 @@ async def git_commit(ctx: AnyContext):
139
139
  )
140
140
  async def git_pull(ctx: AnyContext):
141
141
  ctx.print(stylize_faint("Get directory"))
142
- repo_dir = await get_repo_dir(log_method=ctx.print)
142
+ repo_dir = await get_repo_dir(print_method=ctx.print)
143
143
  ctx.print(stylize_faint("Get current branch"))
144
- current_branch = await get_current_branch(repo_dir, log_method=ctx.print)
144
+ current_branch = await get_current_branch(repo_dir, print_method=ctx.print)
145
145
  remote = ctx.input.remote
146
146
  ctx.print(stylize_faint(f"Pulling from {remote}/{current_branch}"))
147
- await pull(repo_dir, remote, current_branch, log_method=ctx.print)
147
+ await pull(repo_dir, remote, current_branch, print_method=ctx.print)
148
148
 
149
149
 
150
150
  @make_task(
@@ -161,9 +161,9 @@ async def git_pull(ctx: AnyContext):
161
161
  alias="push",
162
162
  )
163
163
  async def git_push(ctx: AnyContext):
164
- repo_dir = await get_repo_dir(log_method=ctx.print)
164
+ repo_dir = await get_repo_dir(print_method=ctx.print)
165
165
  ctx.print(stylize_faint("Get current branch"))
166
- current_branch = await get_current_branch(repo_dir, log_method=ctx.print)
166
+ current_branch = await get_current_branch(repo_dir, print_method=ctx.print)
167
167
  remote = ctx.input.remote
168
168
  ctx.print(stylize_faint(f"Pushing to {remote}/{current_branch}"))
169
- await push(repo_dir, remote, current_branch, log_method=ctx.print)
169
+ await push(repo_dir, remote, current_branch, print_method=ctx.print)
@@ -35,7 +35,7 @@ from zrb.util.git_subtree import add_subtree, load_config, pull_subtree, push_su
35
35
  )
36
36
  async def git_add_subtree(ctx: AnyContext):
37
37
  ctx.print(stylize_faint("Get directory"))
38
- repo_dir = await get_repo_dir(log_method=ctx.print)
38
+ repo_dir = await get_repo_dir(print_method=ctx.print)
39
39
  ctx.print(stylize_faint("Add subtree"))
40
40
  await add_subtree(
41
41
  repo_dir=repo_dir,
@@ -43,7 +43,7 @@ async def git_add_subtree(ctx: AnyContext):
43
43
  repo_url=ctx.input["repo-url"],
44
44
  branch=ctx.input["repo-branch"],
45
45
  prefix=ctx.input["repo-prefix"],
46
- log_method=ctx.print,
46
+ print_method=ctx.print,
47
47
  )
48
48
 
49
49
 
@@ -56,7 +56,7 @@ async def git_add_subtree(ctx: AnyContext):
56
56
  )
57
57
  async def git_pull_subtree(ctx: AnyContext):
58
58
  ctx.print(stylize_faint("Get directory"))
59
- repo_dir = await get_repo_dir(log_method=ctx.print)
59
+ repo_dir = await get_repo_dir(print_method=ctx.print)
60
60
  config = load_config(repo_dir)
61
61
  if not config.data:
62
62
  raise ValueError("No subtree config found")
@@ -69,7 +69,7 @@ async def git_pull_subtree(ctx: AnyContext):
69
69
  prefix=detail.prefix,
70
70
  repo_url=detail.repo_url,
71
71
  branch=detail.branch,
72
- log_method=ctx.print,
72
+ print_method=ctx.print,
73
73
  )
74
74
  except Exception as e:
75
75
  if first_err is None:
@@ -88,7 +88,7 @@ async def git_pull_subtree(ctx: AnyContext):
88
88
  )
89
89
  async def git_push_subtree(ctx: AnyContext):
90
90
  ctx.print(stylize_faint("Get directory"))
91
- repo_dir = await get_repo_dir(log_method=ctx.print)
91
+ repo_dir = await get_repo_dir(print_method=ctx.print)
92
92
  config = load_config(repo_dir)
93
93
  if not config.data:
94
94
  raise ValueError("No subtree config found")
@@ -101,7 +101,7 @@ async def git_push_subtree(ctx: AnyContext):
101
101
  prefix=detail.prefix,
102
102
  repo_url=detail.repo_url,
103
103
  branch=detail.branch,
104
- log_method=ctx.print,
104
+ print_method=ctx.print,
105
105
  )
106
106
  except Exception as e:
107
107
  if first_err is None:
@@ -57,6 +57,7 @@ scaffold_fastapp = Scaffolder(
57
57
  "my_app_name": "{to_snake_case(ctx.input.app)}",
58
58
  "MY_APP_NAME": "{to_snake_case(ctx.input.app).upper()}",
59
59
  "my-secure-password": lambda _: get_random_name(),
60
+ "my-secret-key": lambda _: get_random_name(),
60
61
  },
61
62
  ),
62
63
  # Register fastapp's tasks to project's zrb_init (project_dir/zrb_init.py)
@@ -88,6 +88,7 @@ scaffold_my_app_name_entity = Scaffolder(
88
88
  match=is_in_app_schema_dir,
89
89
  transform={
90
90
  "MyEntity": "{to_pascal_case(ctx.input.entity)}",
91
+ "my_entities": "{to_snake_case(ctx.input.plural)}",
91
92
  "my_column": "{to_snake_case(ctx.input.column)}",
92
93
  },
93
94
  ),
@@ -67,11 +67,11 @@ class MyEntityService(BaseService):
67
67
  @BaseService.route(
68
68
  "/api/v1/my-entities/bulk",
69
69
  methods=["put"],
70
- response_model=MyEntityResponse,
70
+ response_model=list[MyEntityResponse],
71
71
  )
72
72
  async def update_my_entity_bulk(
73
73
  self, my_entity_ids: list[str], data: MyEntityUpdateWithAudit
74
- ) -> MyEntityResponse:
74
+ ) -> list[MyEntityResponse]:
75
75
  await self.my_entity_repository.update_bulk(my_entity_ids, data)
76
76
  return await self.my_entity_repository.get_by_ids(my_entity_ids)
77
77
 
@@ -89,11 +89,11 @@ class MyEntityService(BaseService):
89
89
  @BaseService.route(
90
90
  "/api/v1/my-entities/bulk",
91
91
  methods=["delete"],
92
- response_model=MyEntityResponse,
92
+ response_model=list[MyEntityResponse],
93
93
  )
94
94
  async def delete_my_entity_bulk(
95
95
  self, my_entity_ids: list[str], deleted_by: str
96
- ) -> MyEntityResponse:
96
+ ) -> list[MyEntityResponse]:
97
97
  my_entities = await self.my_entity_repository.get_by_ids(my_entity_ids)
98
98
  await self.my_entity_repository.delete_bulk(my_entity_ids)
99
99
  return my_entities
@@ -106,6 +106,6 @@ class MyEntityService(BaseService):
106
106
  async def delete_my_entity(
107
107
  self, my_entity_id: str, deleted_by: str
108
108
  ) -> MyEntityResponse:
109
- my_entity = await self.my_entity_repository.get_by_id(my_entity.id)
109
+ my_entity = await self.my_entity_repository.get_by_id(my_entity_id)
110
110
  await self.my_entity_repository.delete(my_entity_id)
111
111
  return my_entity
@@ -39,6 +39,7 @@ class MultipleMyEntityResponse(BaseModel):
39
39
 
40
40
 
41
41
  class MyEntity(SQLModel, table=True):
42
+ __tablename__ = "my_entities"
42
43
  id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
43
44
  created_at: datetime.datetime = Field(index=True)
44
45
  created_by: str = Field(index=True)
@@ -3,6 +3,14 @@ from my_app_name._zrb.util import get_existing_module_names, get_existing_schema
3
3
  from zrb import OptionInput, StrInput
4
4
  from zrb.util.string.conversion import pluralize
5
5
 
6
+ run_env_input = OptionInput(
7
+ name="env",
8
+ description="Running environment",
9
+ prompt="Running Environment",
10
+ options=["dev", "prod"],
11
+ default_str="prod",
12
+ )
13
+
6
14
  new_module_input = StrInput(
7
15
  name="module", description="Module name", prompt="New module name"
8
16
  )
@@ -9,8 +9,14 @@ from my_app_name._zrb.group import (
9
9
  app_migrate_group,
10
10
  app_run_group,
11
11
  )
12
+ from my_app_name._zrb.input import run_env_input
12
13
  from my_app_name._zrb.module.add_module_task import add_my_app_name_module
13
- from my_app_name._zrb.util import create_migration, migrate_module, run_microservice
14
+ from my_app_name._zrb.task_util import (
15
+ create_migration,
16
+ migrate_module,
17
+ run_microservice,
18
+ run_my_app_name,
19
+ )
14
20
  from my_app_name._zrb.venv_task import prepare_venv
15
21
 
16
22
  from zrb import CmdTask, EnvFile, EnvMap, Task
@@ -52,6 +58,7 @@ run_monolith = app_run_group.add_task(
52
58
  CmdTask(
53
59
  name="run-monolith-my-app-name",
54
60
  description="🗿 Run My App Name as a monolith",
61
+ input=run_env_input,
55
62
  env=[
56
63
  EnvFile(path=os.path.join(APP_DIR, "template.env")),
57
64
  EnvMap(vars=MONOLITH_ENV_VARS),
@@ -59,7 +66,7 @@ run_monolith = app_run_group.add_task(
59
66
  cwd=APP_DIR,
60
67
  cmd=[
61
68
  ACTIVATE_VENV_SCRIPT,
62
- 'fastapi dev main.py --port "${MY_APP_NAME_PORT}"',
69
+ run_my_app_name,
63
70
  ],
64
71
  render_cmd=False,
65
72
  retries=2,
@@ -0,0 +1,100 @@
1
+ import os
2
+
3
+ from my_app_name._zrb.config import (
4
+ ACTIVATE_VENV_SCRIPT,
5
+ APP_DIR,
6
+ MICROSERVICES_ENV_VARS,
7
+ MONOLITH_ENV_VARS,
8
+ )
9
+ from my_app_name._zrb.input import run_env_input
10
+ from my_app_name._zrb.util import (
11
+ cd_module_script,
12
+ run_my_app_name,
13
+ set_create_migration_db_url_env,
14
+ set_env,
15
+ set_module_env,
16
+ )
17
+
18
+ from zrb import Cmd, CmdTask, EnvFile, EnvMap, StrInput, Task
19
+ from zrb.util.string.conversion import to_snake_case
20
+
21
+
22
+ def create_migration(name: str, module: str) -> Task:
23
+ return CmdTask(
24
+ name=f"create-my-app-name-{name}-migration",
25
+ description=f"🧩 Create My App Name {name.capitalize()} DB migration",
26
+ input=StrInput(
27
+ name="message",
28
+ description="Migration message",
29
+ prompt="Migration message",
30
+ allow_empty=False,
31
+ ),
32
+ env=EnvFile(path=os.path.join(APP_DIR, "template.env")),
33
+ cwd=APP_DIR,
34
+ cmd=[
35
+ ACTIVATE_VENV_SCRIPT,
36
+ set_create_migration_db_url_env(module),
37
+ set_module_env(module),
38
+ cd_module_script(module),
39
+ "alembic upgrade head",
40
+ Cmd(
41
+ "alembic revision --autogenerate -m {double_quote(ctx.input.message)}",
42
+ auto_render=True,
43
+ ),
44
+ ],
45
+ render_cmd=False,
46
+ retries=2,
47
+ )
48
+
49
+
50
+ def migrate_module(name: str, module: str, as_microservices: bool) -> Task:
51
+ env_vars = (
52
+ dict(MICROSERVICES_ENV_VARS) if as_microservices else dict(MONOLITH_ENV_VARS)
53
+ )
54
+ if as_microservices:
55
+ env_vars["MY_APP_NAME_MODULES"] = to_snake_case(module)
56
+ return CmdTask(
57
+ name=(
58
+ f"migrate-my-app-name-{name}"
59
+ if as_microservices
60
+ else f"migrate-{name}-on-monolith"
61
+ ),
62
+ description=f"🧩 Run My App Name {name.capitalize()} DB migration",
63
+ env=[
64
+ EnvFile(path=os.path.join(APP_DIR, "template.env")),
65
+ EnvMap(vars=env_vars),
66
+ ],
67
+ cwd=APP_DIR,
68
+ cmd=[
69
+ ACTIVATE_VENV_SCRIPT,
70
+ cd_module_script(module),
71
+ "alembic upgrade head",
72
+ ],
73
+ render_cmd=False,
74
+ retries=2,
75
+ )
76
+
77
+
78
+ def run_microservice(name: str, port: int, module: str) -> Task:
79
+ return CmdTask(
80
+ name=f"run-my-app-name-{name}",
81
+ description=f"🧩 Run My App Name {name.capitalize()}",
82
+ input=run_env_input,
83
+ env=[
84
+ EnvFile(path=os.path.join(APP_DIR, "template.env")),
85
+ EnvMap(
86
+ vars={
87
+ **MICROSERVICES_ENV_VARS,
88
+ }
89
+ ),
90
+ ],
91
+ cwd=APP_DIR,
92
+ cmd=[
93
+ ACTIVATE_VENV_SCRIPT,
94
+ set_env("MY_APP_NAME_MODULES", module),
95
+ set_env("MY_APP_NAME_PORT", f"{port}"),
96
+ run_my_app_name,
97
+ ],
98
+ render_cmd=False,
99
+ retries=2,
100
+ )
@@ -1,95 +1,15 @@
1
1
  import os
2
2
  import platform
3
3
 
4
- from my_app_name._zrb.config import (
5
- ACTIVATE_VENV_SCRIPT,
6
- APP_DIR,
7
- MICROSERVICES_ENV_VARS,
8
- MONOLITH_ENV_VARS,
9
- )
10
-
11
- from zrb import Cmd, CmdTask, EnvFile, EnvMap, StrInput, Task
12
- from zrb.util.string.conversion import double_quote, to_snake_case
13
-
14
-
15
- def create_migration(name: str, module: str) -> Task:
16
- return CmdTask(
17
- name=f"create-my-app-name-{name}-migration",
18
- description=f"🧩 Create My App Name {name.capitalize()} DB migration",
19
- input=StrInput(
20
- name="message",
21
- description="Migration message",
22
- prompt="Migration message",
23
- allow_empty=False,
24
- ),
25
- env=EnvFile(path=os.path.join(APP_DIR, "template.env")),
26
- cwd=APP_DIR,
27
- cmd=[
28
- ACTIVATE_VENV_SCRIPT,
29
- set_create_migration_db_url_env(module),
30
- set_module_env(module),
31
- cd_module_script(module),
32
- "alembic upgrade head",
33
- Cmd(
34
- "alembic revision --autogenerate -m {double_quote(ctx.input.message)}",
35
- auto_render=True,
36
- ),
37
- ],
38
- render_cmd=False,
39
- retries=2,
40
- )
41
-
4
+ from my_app_name._zrb.config import APP_DIR
42
5
 
43
- def migrate_module(name: str, module: str, as_microservices: bool) -> Task:
44
- env_vars = (
45
- dict(MICROSERVICES_ENV_VARS) if as_microservices else dict(MONOLITH_ENV_VARS)
46
- )
47
- if as_microservices:
48
- env_vars["MY_APP_NAME_MODULES"] = to_snake_case(module)
49
- return CmdTask(
50
- name=(
51
- f"migrate-my-app-name-{name}"
52
- if as_microservices
53
- else f"migrate-{name}-on-monolith"
54
- ),
55
- description=f"🧩 Run My App Name {name.capitalize()} DB migration",
56
- env=[
57
- EnvFile(path=os.path.join(APP_DIR, "template.env")),
58
- EnvMap(vars=env_vars),
59
- ],
60
- cwd=APP_DIR,
61
- cmd=[
62
- ACTIVATE_VENV_SCRIPT,
63
- cd_module_script(module),
64
- "alembic upgrade head",
65
- ],
66
- render_cmd=False,
67
- retries=2,
68
- )
6
+ from zrb import AnyContext
7
+ from zrb.util.string.conversion import double_quote, to_snake_case
69
8
 
70
9
 
71
- def run_microservice(name: str, port: int, module: str) -> Task:
72
- return CmdTask(
73
- name=f"run-my-app-name-{name}",
74
- description=f"🧩 Run My App Name {name.capitalize()}",
75
- env=[
76
- EnvFile(path=os.path.join(APP_DIR, "template.env")),
77
- EnvMap(
78
- vars={
79
- **MICROSERVICES_ENV_VARS,
80
- }
81
- ),
82
- ],
83
- cwd=APP_DIR,
84
- cmd=[
85
- ACTIVATE_VENV_SCRIPT,
86
- set_env("MY_APP_NAME_MODULES", module),
87
- set_env("MY_APP_NAME_PORT", f"{port}"),
88
- 'fastapi dev main.py --port "${MY_APP_NAME_PORT}"',
89
- ],
90
- render_cmd=False,
91
- retries=2,
92
- )
10
+ def run_my_app_name(ctx: AnyContext) -> str:
11
+ subcommand = "dev" if ctx.input.env == "dev" else "run"
12
+ return f'fastapi {subcommand} main.py --port "${{MY_APP_NAME_PORT}}"'
93
13
 
94
14
 
95
15
  def get_existing_module_names() -> list[str]:
@@ -122,9 +122,19 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
122
122
  return self._ensure_one(rows)
123
123
 
124
124
  async def get_by_ids(self, id_list: list[str]) -> list[ResponseModel]:
125
- return await self._select_to_response(
125
+ rows = await self._select_to_response(
126
126
  lambda q: q.where(self.db_model.id.in_(id_list))
127
127
  )
128
+ # raise error if any id not in id_list
129
+ existing_id_list = [row.id for row in rows]
130
+ inexist_id_list = [id for id in id_list if id not in existing_id_list]
131
+ if len(inexist_id_list) > 0:
132
+ raise NotFoundError(
133
+ f"{self.entity_name} not found, inexist ids: {', '.join(inexist_id_list)}"
134
+ )
135
+ # sort rows
136
+ row_dict = {row.id: row for row in rows}
137
+ return [row_dict[id] for id in id_list]
128
138
 
129
139
  async def count(self, filter: str | None = None) -> int:
130
140
  count_statement = select(func.count(1)).select_from(self.db_model)
@@ -184,21 +194,22 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
184
194
 
185
195
  async def create_bulk(self, data_list: list[CreateModel]) -> list[DBModel]:
186
196
  now = datetime.datetime.now(datetime.timezone.utc)
187
- data_dicts = [
197
+ data_dict_list = [
188
198
  self._model_to_data_dict(data, created_at=now, id=ulid.new().str)
189
199
  for data in data_list
190
200
  ]
201
+ id_list = [data_dict["id"] for data_dict in data_dict_list]
191
202
  async with self._session_scope() as session:
192
203
  await self._execute_statement(
193
- session, insert(self.db_model).values(data_dicts)
204
+ session, insert(self.db_model).values(data_dict_list)
194
205
  )
195
- id_list = [d["id"] for d in data_dicts]
196
206
  statement = select(self.db_model).where(self.db_model.id.in_(id_list))
197
207
  result = await self._execute_statement(session, statement)
198
- return [
199
- self.db_model(**entity.model_dump())
208
+ row_dict = {
209
+ entity.id: self.db_model(**entity.model_dump())
200
210
  for entity in result.scalars().all()
201
- ]
211
+ }
212
+ return [row_dict[id] for id in id_list]
202
213
 
203
214
  async def delete(self, id: str) -> DBModel:
204
215
  async with self._session_scope() as session:
@@ -220,11 +231,15 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
220
231
  await self._execute_statement(
221
232
  session, delete(self.db_model).where(self.db_model.id.in_(id_list))
222
233
  )
223
- return [self.db_model(**entity.model_dump()) for entity in entities]
234
+ row_dict = {
235
+ entity.id: self.db_model(**entity.model_dump()) for entity in entities
236
+ }
237
+ return [row_dict[id] for id in id_list]
224
238
 
225
239
  async def update(self, id: str, data: UpdateModel) -> DBModel:
226
240
  now = datetime.datetime.now(datetime.timezone.utc)
227
241
  update_data = self._model_to_data_dict(data, updated_at=now)
242
+ update_data = {k: v for k, v in update_data.items() if v is not None}
228
243
  async with self._session_scope() as session:
229
244
  statement = (
230
245
  update(self.db_model)
@@ -256,7 +271,8 @@ class BaseDBRepository(Generic[DBModel, ResponseModel, CreateModel, UpdateModel]
256
271
  result = await self._execute_statement(
257
272
  session, select(self.db_model).where(self.db_model.id.in_(id_list))
258
273
  )
259
- return [
260
- self.db_model(**entity.model_dump())
274
+ row_dict = {
275
+ entity.id: self.db_model(**entity.model_dump())
261
276
  for entity in result.scalars().all()
262
- ]
277
+ }
278
+ return [row_dict[id] for id in id_list]
@@ -56,7 +56,6 @@ class BaseService:
56
56
  response_model: Any = None,
57
57
  status_code: int | None = None,
58
58
  tags: list[str | Enum] | None = None,
59
- dependencies: Sequence[params.Depends] | None = None,
60
59
  summary: str | None = None,
61
60
  description: str = None,
62
61
  deprecated: bool | None = None,
@@ -160,56 +159,62 @@ def _create_direct_client_method(logger: Logger, func: Callable, service: BaseSe
160
159
  return client_method
161
160
 
162
161
 
163
- def _create_api_client_method(logger: Logger, param: RouteParam, base_url: str):
162
+ def _create_api_client_method(logger: Logger, route_param: RouteParam, base_url: str):
164
163
  async def client_method(*args, **kwargs):
165
- url = base_url + param.path
164
+ url = base_url + route_param.path
166
165
  method = (
167
- param.methods[0].lower()
168
- if isinstance(param.methods, list)
169
- else param.methods.lower()
166
+ route_param.methods[0].lower()
167
+ if isinstance(route_param.methods, list)
168
+ else route_param.methods.lower()
170
169
  )
171
170
  # Get the signature of the original function
172
- sig = inspect.signature(param.func)
171
+ sig = inspect.signature(route_param.func)
173
172
  # Bind the arguments to the signature
174
173
  bound_args = sig.bind(*args, **kwargs)
175
174
  bound_args.apply_defaults()
176
175
  # Analyze parameters
177
- params = list(sig.parameters.values())
178
- body_params = [
179
- p
180
- for p in params
181
- if p.name != "self" and p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
176
+ function_params = list(sig.parameters.values())
177
+ body_param_names = [
178
+ p.name
179
+ for p in function_params
180
+ if (
181
+ p.name != "self"
182
+ and f"{{{p.name}}}" not in route_param.path
183
+ and p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
184
+ and (
185
+ method not in ["get", "delete"]
186
+ or (method == "delete" and p.annotation not in [str, float, bool])
187
+ )
188
+ )
182
189
  ]
183
190
  # Prepare the request
184
191
  path_params = {}
185
192
  query_params = {}
186
- body = {}
193
+ body_params = {}
187
194
  for name, value in bound_args.arguments.items():
188
195
  if name == "self":
189
196
  continue
190
- if f"{{{name}}}" in param.path:
197
+ if f"{{{name}}}" in route_param.path:
191
198
  path_params[name] = value
192
- elif isinstance(value, BaseModel):
193
- body = _parse_api_param(value)
194
- elif method in ["get", "delete"]:
199
+ elif name not in body_param_names:
195
200
  query_params[name] = _parse_api_param(value)
196
- elif len(body_params) == 1 and name == body_params[0].name:
201
+ elif len(body_param_names) == 1 and name == body_param_names[0]:
197
202
  # If there's only one body parameter, use its value directly
198
- body = _parse_api_param(value)
203
+ body_params = _parse_api_param(value)
199
204
  else:
200
- body[name] = _parse_api_param(value)
205
+ body_params[name] = _parse_api_param(value)
201
206
  # Format the URL with path parameters
202
207
  url = url.format(**path_params)
203
208
  logger.info(
204
- f"Sending request to {url} with method {method}, json={body}, params={query_params}" # noqa
209
+ f"Sending request to {url} with method {method}, json={body_params}, params={query_params}" # noqa
205
210
  )
206
211
  async with httpx.AsyncClient() as client:
207
- if method in ["get", "delete"]:
208
- response = await getattr(client, method)(url, params=query_params)
209
- else:
210
- response = await getattr(client, method)(
211
- url, json=body, params=query_params
212
- )
212
+ response = await client.request(
213
+ method=method,
214
+ url=url,
215
+ params=query_params,
216
+ json=None if method == "get" else body_params,
217
+ )
213
218
  logger.info(
214
219
  f"Received response: status={response.status_code}, content={response.content}"
215
220
  )
@@ -8,11 +8,26 @@ class NotFoundError(HTTPException):
8
8
  super().__init__(404, {"message": message}, headers)
9
9
 
10
10
 
11
+ class ForbiddenError(HTTPException):
12
+ def __init__(self, message: str, headers: Dict[str, str] | None = None) -> None:
13
+ super().__init__(403, {"message": message}, headers)
14
+
15
+
16
+ class UnauthorizedError(HTTPException):
17
+ def __init__(self, message: str, headers: Dict[str, str] | None = None) -> None:
18
+ super().__init__(401, {"message": message}, headers)
19
+
20
+
11
21
  class InvalidValueError(HTTPException):
12
22
  def __init__(self, message: str, headers: Dict[str, str] | None = None) -> None:
13
23
  super().__init__(422, {"message": message}, headers)
14
24
 
15
25
 
26
+ class InternalServerError(HTTPException):
27
+ def __init__(self, message: str, headers: Dict[str, str] | None = None) -> None:
28
+ super().__init__(500, {"message": message}, headers)
29
+
30
+
16
31
  class ClientAPIError(HTTPException):
17
32
  def __init__(
18
33
  self, status_code: int, message: str, headers: Dict[str, str] | None = None