magic-pocket-cli 0.2.0__tar.gz → 0.2.2__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 (64) hide show
  1. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/PKG-INFO +2 -2
  2. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/awscontainer_cli.py +6 -3
  3. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/deploy_cli.py +2 -1
  4. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/destroy_cli.py +2 -1
  5. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/neon_cli.py +2 -1
  6. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/rds_cli.py +2 -1
  7. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/waf_cli.py +1 -1
  8. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/django_cli.py +5 -5
  9. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/builders/depot.py +1 -1
  10. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/cloudformation.py +6 -2
  11. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/ecr.py +2 -2
  12. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/efs.py +2 -1
  13. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/lambdahandler.py +23 -0
  14. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/awscontainer.py +31 -0
  15. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/cloudfront.py +7 -3
  16. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/cloudfront_waf.py +2 -1
  17. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/neon.py +10 -4
  18. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/rds.py +24 -15
  19. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/tidb.py +2 -2
  20. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/upstash.py +5 -3
  21. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pyproject.toml +2 -2
  22. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/.gitignore +0 -0
  23. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/__init__.py +0 -0
  24. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/__init__.py +0 -0
  25. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/aws_auth.py +0 -0
  26. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/cloudfront_cli.py +0 -0
  27. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
  28. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
  29. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/dsql_cli.py +0 -0
  30. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/main_cli.py +0 -0
  31. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/migrate_cli.py +0 -0
  32. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/permissions_cli.py +0 -0
  33. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/runtime_config_cli.py +0 -0
  34. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/s3_cli.py +0 -0
  35. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/status_cli.py +0 -0
  36. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/tidb_cli.py +0 -0
  37. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/cli/vpc_cli.py +0 -0
  38. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/mediator.py +0 -0
  39. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/__init__.py +0 -0
  40. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/__init__.py +0 -0
  41. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/builders/__init__.py +0 -0
  42. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
  43. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/builders/docker.py +0 -0
  44. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
  45. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/s3_utils.py +0 -0
  46. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/aws/state.py +0 -0
  47. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/cloudfront_acm.py +0 -0
  48. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/cloudfront_keys.py +0 -0
  49. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/dsql.py +0 -0
  50. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/s3.py +0 -0
  51. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/resources/vpc.py +0 -0
  52. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/awscontainer.yaml +0 -0
  53. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
  54. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
  55. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
  56. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
  57. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
  58. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
  59. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
  60. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
  61. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/init/django-dotenv.env +0 -0
  62. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/init/django-settings.py +0 -0
  63. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
  64. {magic_pocket_cli-0.2.0 → magic_pocket_cli-0.2.2}/pocket_cli/templates/init/pocket_simple.toml +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: magic-pocket-cli
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: CLI and deploy tools for magic-pocket.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: awscrt>=0.19.0
7
7
  Requires-Dist: click>=8.1.7
8
8
  Requires-Dist: deepdiff>=6.7.1
9
9
  Requires-Dist: jinja2>=3.1.3
10
- Requires-Dist: magic-pocket>=0.2.0
10
+ Requires-Dist: magic-pocket>=0.2.2
11
11
  Requires-Dist: pathspec>=1.0.4
12
12
  Requires-Dist: python-on-whales>=0.68.0
13
13
  Requires-Dist: pyyaml>=6.0.1
@@ -191,7 +191,8 @@ def _resolve_lambda_target_handlers(
191
191
  context: Context, handler_name: str | None
192
192
  ) -> list[str]:
193
193
  """reload-env / status-env の対象 handler を解決する。"""
194
- assert context.awscontainer is not None
194
+ if context.awscontainer is None:
195
+ raise RuntimeError("awscontainer is not configured")
195
196
  handlers = context.awscontainer.handlers
196
197
  if handler_name:
197
198
  if handler_name not in handlers:
@@ -204,7 +205,8 @@ def _resolve_lambda_target_handlers(
204
205
 
205
206
 
206
207
  def _function_name(context: Context, handler_key: str) -> str:
207
- assert context.awscontainer is not None
208
+ if context.awscontainer is None:
209
+ raise RuntimeError("awscontainer is not configured")
208
210
  # deploy 側 (LambdaHandlerContext.function_name = resource_prefix + key) と同じ
209
211
  # 正準名を参照する。slug から再構成すると prefix_template / namespace
210
212
  # (既定 `pocket`) を取りこぼすため、handler context の値をそのまま使う。
@@ -306,7 +308,8 @@ def status_env(stage, handler):
306
308
  any_drift = True
307
309
  if any_drift:
308
310
  echo.warning(
309
- "drift があります。`pocket resource awscontainer reload-env` で同期できます。"
311
+ "drift があります。"
312
+ "`pocket resource awscontainer reload-env` で同期できます。"
310
313
  )
311
314
  else:
312
315
  echo.success("drift なし。Lambda env と secrets は同期されています。")
@@ -70,7 +70,8 @@ def get_resources(context: Context, *, state_bucket: str = ""):
70
70
 
71
71
 
72
72
  def _create_state_store(context: Context) -> StateStore:
73
- assert context.general
73
+ if not context.general:
74
+ raise RuntimeError("general context is not configured")
74
75
  resource_prefix = context.general.prefix_template.format(
75
76
  stage=context.stage,
76
77
  project=context.project_name,
@@ -19,7 +19,8 @@ from pocket_cli.resources.vpc import Vpc
19
19
 
20
20
 
21
21
  def _create_state_store(context: Context) -> StateStore:
22
- assert context.general
22
+ if not context.general:
23
+ raise RuntimeError("general context is not configured")
23
24
  resource_prefix = context.general.prefix_template.format(
24
25
  stage=context.stage,
25
26
  project=context.project_name,
@@ -53,7 +53,8 @@ def branch_out(stage, base_stage):
53
53
  base_neon = get_neon_resource(base_stage)
54
54
  if not base_neon.working:
55
55
  raise Exception("Base stage is not working")
56
- assert base_neon.branch
56
+ if not base_neon.branch:
57
+ raise RuntimeError("base stage branch is not resolved")
57
58
  neon.create_branch(base_neon.branch)
58
59
  echo.success("New branch was created")
59
60
 
@@ -56,7 +56,8 @@ def destroy(stage):
56
56
 
57
57
 
58
58
  def _print_endpoint(r: Rds):
59
- assert r.cluster
59
+ if not r.cluster:
60
+ raise RuntimeError("RDS cluster is not available")
60
61
  echo.success("Endpoint: %s" % r.cluster.get("Endpoint", ""))
61
62
  echo.success("Port: %s" % r.cluster.get("Port", ""))
62
63
  echo.success("Database: %s" % r.context.database_name)
@@ -91,7 +91,7 @@ def _detect_self_ipv4() -> str:
91
91
  """1 次: AWS checkip、fallback: ipify。/32 を付けた CIDR を返す。"""
92
92
  for url in ("https://checkip.amazonaws.com", "https://api.ipify.org"):
93
93
  try:
94
- with urllib.request.urlopen(url, timeout=5) as r:
94
+ with urllib.request.urlopen(url, timeout=5) as r: # noqa: S310 固定 URL (checkip/ipify) + timeout 済
95
95
  ip = r.read().decode("utf-8").strip()
96
96
  if ip:
97
97
  return f"{ip}/32"
@@ -189,7 +189,7 @@ def build(stage: str, allow_dirty: bool):
189
189
  try:
190
190
  tag = get_commit_hash()
191
191
  except RuntimeError as e:
192
- raise click.ClickException(str(e))
192
+ raise click.ClickException(str(e)) from e
193
193
  context = Context.from_toml(stage=stage)
194
194
  target = build_image(context, tag=tag)
195
195
  echo.success("built and pushed: %s" % target)
@@ -281,10 +281,10 @@ def upload_collected_staticfiles(stage: str):
281
281
  echo.info("Bucket: %s" % s3_bucket_name)
282
282
  echo.info("Location: %s" % s3_location)
283
283
  echo.info("Uploading static files...")
284
- run(
284
+ run( # noqa: S602 aws s3 sync を pocket.toml 設定値から構築 (信頼境界内)
285
285
  "aws s3 sync %s s3://%s/%s/ --delete"
286
286
  % (local_storage["OPTIONS"]["location"], s3_bucket_name, s3_location),
287
- shell=True,
287
+ shell=True, # nosemgrep
288
288
  check=True,
289
289
  )
290
290
 
@@ -319,7 +319,7 @@ def collectstatic_locally(stage: str):
319
319
  set_staticfiles_override_env(local_storage)
320
320
  cmd = _build_python_command(["manage.py", "collectstatic", "--noinput"])
321
321
  project_dir = _get_project_dir(stage)
322
- run(cmd, check=True, cwd=project_dir)
322
+ run(cmd, check=True, cwd=project_dir) # noqa: S603 shell=False + 制御された引数
323
323
  clear_staticfiles_override_env()
324
324
 
325
325
 
@@ -409,4 +409,4 @@ def upload(storage, stage, delete, dryrun):
409
409
  if dryrun:
410
410
  cmd += " --dryrun"
411
411
  print(cmd)
412
- run(cmd, shell=True, check=True)
412
+ run(cmd, shell=True, check=True) # noqa: S602 aws s3 sync を pocket.toml 設定値から構築 (信頼境界内) # nosemgrep
@@ -77,7 +77,7 @@ class DepotBuilder:
77
77
  env[key] = value
78
78
  if self.project_id:
79
79
  env["DEPOT_PROJECT_ID"] = self.project_id
80
- subprocess.run(cmd, check=True, env=env)
80
+ subprocess.run(cmd, check=True, env=env) # noqa: S603 shell=False + 制御された引数
81
81
  print("Depot ビルド完了")
82
82
 
83
83
  def delete(self) -> None:
@@ -645,10 +645,14 @@ class ContainerStack(Stack):
645
645
  return {}
646
646
 
647
647
  def _resolve_vpc_zone_count(self) -> int:
648
- assert self.context.vpc, "VPC context is required"
648
+ if not self.context.vpc:
649
+ raise RuntimeError("VPC context is required")
649
650
  vpc_stack = VpcStack(self.context.vpc)
650
651
  output = vpc_stack.output
651
- assert output, f"VPC stack '{vpc_stack.name}' の output が取得できません"
652
+ if not output:
653
+ raise RuntimeError(
654
+ f"VPC stack '{vpc_stack.name}' の output が取得できません"
655
+ )
652
656
  prefix = vpc_stack.export["private_subnet_"]
653
657
  count = 0
654
658
  for i in range(1, 20):
@@ -110,11 +110,11 @@ class Ecr:
110
110
  repositoryName=self.name,
111
111
  imageIds=[{"imageTag": source_tag}],
112
112
  )["images"]
113
- except self.client.exceptions.RepositoryNotFoundException:
113
+ except self.client.exceptions.RepositoryNotFoundException as e:
114
114
  raise ValueError(
115
115
  "ECR repository '%s' が存在しません。"
116
116
  "先に `pocket django build` を実行してください。" % self.name
117
- )
117
+ ) from e
118
118
  if not images:
119
119
  raise ValueError(
120
120
  "image :%s が ECR repository '%s' に存在しません。"
@@ -123,7 +123,8 @@ class Efs:
123
123
  def status(self) -> ResourceStatus:
124
124
  if not self.exists():
125
125
  return "NOEXIST"
126
- assert self.description
126
+ if not self.description:
127
+ raise RuntimeError("EFS description is not available")
127
128
  if self.description["LifeCycleState"] != "available":
128
129
  return "PROGRESS"
129
130
  if self.lifecycle_policies != [
@@ -79,6 +79,29 @@ class LambdaHandler:
79
79
  if wait:
80
80
  self.wait_update()
81
81
 
82
+ def get_environment(self) -> dict[str, str]:
83
+ """Lambda の現状 Environment.Variables を取得する (ImportValue / secret は
84
+ 解決済みの実値が返る)。function 未作成なら空 dict。"""
85
+ try:
86
+ config = self.client.get_function_configuration(FunctionName=self.name)
87
+ except ClientError:
88
+ return {}
89
+ return dict(config.get("Environment", {}).get("Variables", {}))
90
+
91
+ def update_environment(self, env: dict[str, str], wait=True):
92
+ """Environment.Variables を side-channel で直接更新する。
93
+
94
+ `update()` は update_function_code (code のみ) で Environment を更新しない。
95
+ deploy 時に CFn を介さず env を同期したい用途 (DEPLOY_HASH 同期 / reload-env)
96
+ で使う。
97
+ """
98
+ self.client.update_function_configuration(
99
+ FunctionName=self.name, Environment={"Variables": env}
100
+ )
101
+ print(f"lambda function {self.name} environment was updated.")
102
+ if wait:
103
+ self.wait_update()
104
+
82
105
  def wait_update(self, interval=3, limit=60):
83
106
  print(f"waiting lambda fanction {self.name} update.", end="", flush=True)
84
107
  for _i in range(limit // interval):
@@ -232,6 +232,37 @@ class AwsContainer:
232
232
  if not self.stack.yaml_synced:
233
233
  self.stack.update()
234
234
  self.stack.wait_status("COMPLETED", timeout=600, interval=10)
235
+ self.ensure_post_deploy_state()
236
+
237
+ def ensure_post_deploy_state(self, mediator: Mediator | None = None):
238
+ """deploy 完了後に Lambda env の DEPLOY_HASH を冪等に同期する。
239
+
240
+ LambdaHandler.update() は update_function_code (code) のみで Environment を
241
+ 更新しない。Lambda env は CFn stack.update() 経由でしか書き換わらないため、
242
+ stack が status==COMPLETED / yaml_synced と判定されて update() がスキップ
243
+ されると DEPLOY_HASH が旧値に固着する。すると Django は古い hash の static
244
+ URL (/static/<旧hash>/...) を生成する一方、CloudFront の deploy_hash 関数は
245
+ 毎 deploy 追従するため、静的アセットが全滅する (403)。
246
+
247
+ cloudfront の KVS 書き込みと同じ philosophy で、deploy フロー末尾で stack
248
+ 状態によらず side-channel で冪等に同期する (reload-env / waf ip と同様)。
249
+ deploy_resources の post-deploy hook からも呼ばれるため、wait_status が
250
+ timeout して次 deploy で update() がスキップされた場合でも自己治癒する。
251
+ """
252
+ deploy_hash = self.context.envs.get("DEPLOY_HASH")
253
+ if not deploy_hash:
254
+ return
255
+ for key, handler in self.handlers.items():
256
+ if handler.status == "NOEXIST":
257
+ continue
258
+ current = handler.get_environment()
259
+ if current.get("DEPLOY_HASH") == deploy_hash:
260
+ continue
261
+ echo.log(
262
+ "[%s] Lambda env DEPLOY_HASH を同期します (%s → %s)"
263
+ % (key, current.get("DEPLOY_HASH"), deploy_hash)
264
+ )
265
+ handler.update_environment({**current, "DEPLOY_HASH": deploy_hash})
235
266
 
236
267
  def get_host(self, key: str):
237
268
  handler = self.handlers[key]
@@ -204,7 +204,9 @@ class CloudFront:
204
204
  if route.build and not skip_build:
205
205
  echo.info("ビルド実行: %s" % route.build)
206
206
  try:
207
- subprocess.run(route.build, shell=True, check=True)
207
+ # route.build pocket.toml の build コマンド (設定者 =
208
+ # デプロイ実行者)。意図的な shell 実行のため S602 / semgrep を抑制。
209
+ subprocess.run(route.build, shell=True, check=True) # noqa: S602 # nosemgrep
208
210
  except subprocess.CalledProcessError as e:
209
211
  echo.danger(
210
212
  "build コマンドが失敗しました (exit %d): %s"
@@ -224,7 +226,8 @@ class CloudFront:
224
226
 
225
227
  def _upload_route(self, route: RouteContext):
226
228
  s3_prefix = (route.origin_path + route.path_pattern.rstrip("/*")).lstrip("/")
227
- assert route.build_dir
229
+ if not route.build_dir:
230
+ raise RuntimeError("route.build_dir is not set")
228
231
  local_dir = Path(route.build_dir)
229
232
  uploaded_keys: set[str] = set()
230
233
  for file in local_dir.rglob("*"):
@@ -338,7 +341,8 @@ class CloudFront:
338
341
  def _ensure_redirect_from_website(self):
339
342
  if not self.context.redirect_from:
340
343
  return
341
- assert self.context.domain, "domain is required when redirect_from is set"
344
+ if not self.context.domain:
345
+ raise RuntimeError("domain is required when redirect_from is set")
342
346
  for redirect_from in self.context.redirect_from:
343
347
  self.s3_client.put_bucket_website(
344
348
  Bucket=redirect_from.domain,
@@ -45,7 +45,8 @@ class CloudFrontWaf:
45
45
  echo.log("WAF (IPSet + WebACL) を作成中 (us-east-1)...")
46
46
  self.stack.create()
47
47
  self.stack.wait_status("COMPLETED", timeout=300, interval=10)
48
- assert self.context.waf is not None
48
+ if self.context.waf is None:
49
+ raise RuntimeError("waf context is not configured")
49
50
  if self.context.waf.enable_ip_set:
50
51
  echo.info(
51
52
  "WAF を作成しました。`pocket waf ip add self --name %s --stage %s` "
@@ -80,7 +80,7 @@ class NeonApi:
80
80
 
81
81
  def get(self, path):
82
82
  logger.info("GET %s" % self.endpoint + path)
83
- res = requests.get(self.endpoint + path, headers=self.header)
83
+ res = requests.get(self.endpoint + path, headers=self.header, timeout=30)
84
84
  logger.debug(res.status_code)
85
85
  logger.debug(json.dumps(res.json(), indent=2))
86
86
  if 200 <= res.status_code < 300:
@@ -95,7 +95,9 @@ class NeonApi:
95
95
  def post(self, path, data=None):
96
96
  logger.warning("POST %s" % self.endpoint + path)
97
97
  logger.debug(json.dumps(data, indent=2))
98
- res = requests.post(self.endpoint + path, headers=self.header, json=data)
98
+ res = requests.post(
99
+ self.endpoint + path, headers=self.header, json=data, timeout=30
100
+ )
99
101
  logger.debug(res.status_code)
100
102
  logger.debug(json.dumps(res.json(), indent=2))
101
103
  if 200 <= res.status_code < 300:
@@ -109,7 +111,9 @@ class NeonApi:
109
111
  def delete(self, path, data=None):
110
112
  logger.warning("DELETE %s" % self.endpoint + path)
111
113
  logger.debug(json.dumps(data, indent=2))
112
- res = requests.delete(self.endpoint + path, headers=self.header, json=data)
114
+ res = requests.delete(
115
+ self.endpoint + path, headers=self.header, json=data, timeout=30
116
+ )
113
117
  logger.debug(res.status_code)
114
118
  logger.debug(json.dumps(res.json(), indent=2))
115
119
  if 200 <= res.status_code < 300:
@@ -192,7 +196,9 @@ class Neon:
192
196
  組織キー: GET /projects で一覧取得し name で検索。
193
197
  プロジェクトスコープキー: エラーから project_id を取得し直接アクセス。
194
198
  """
195
- res = requests.get(self.api.endpoint + "projects", headers=self.api.header)
199
+ res = requests.get(
200
+ self.api.endpoint + "projects", headers=self.api.header, timeout=30
201
+ )
196
202
  if 200 <= res.status_code < 300:
197
203
  for p in res.json().get("projects", []):
198
204
  if p["name"] == self.context.project_name:
@@ -106,9 +106,9 @@ class Rds:
106
106
 
107
107
  @property
108
108
  def master_user_secret_arn(self) -> str | None:
109
- if self.context.password_strategy == "static":
109
+ if self.context.password_strategy == "static": # noqa: S105 戦略名/保存先種別であって secret 値ではない
110
110
  # secret_store=ssm の場合は Secrets Manager に secret を作らない。
111
- if self.context.secret_store != "sm":
111
+ if self.context.secret_store != "sm": # noqa: S105 戦略名/保存先種別であって secret 値ではない
112
112
  return None
113
113
  return self._static_secret["ARN"] if self._static_secret else None
114
114
  if self.cluster and "MasterUserSecret" in self.cluster:
@@ -118,8 +118,8 @@ class Rds:
118
118
  @property
119
119
  def static_ssm_param_name(self) -> str | None:
120
120
  """static + secret_store=ssm のとき、認証情報を保存する SSM パラメータ名。"""
121
- if self.context.password_strategy == "static" and (
122
- self.context.secret_store == "ssm"
121
+ if self.context.password_strategy == "static" and ( # noqa: S105 戦略名/保存先種別であって secret 値ではない
122
+ self.context.secret_store == "ssm" # noqa: S105 戦略名/保存先種別であって secret 値ではない
123
123
  ):
124
124
  return self.context.credentials_secret_name
125
125
  return None
@@ -128,7 +128,7 @@ class Rds:
128
128
  def master_user_secret_kms_key_id(self) -> str | None:
129
129
  # static は既定の aws/secretsmanager キーで暗号化するため、
130
130
  # secretsmanager:GetSecretValue のみで復号でき KMS の明示付与は不要。
131
- if self.context.password_strategy == "static":
131
+ if self.context.password_strategy == "static": # noqa: S105 戦略名/保存先種別であって secret 値ではない
132
132
  return None
133
133
  if self.cluster and "MasterUserSecret" in self.cluster:
134
134
  return self.cluster["MasterUserSecret"].get("KmsKeyId")
@@ -206,7 +206,7 @@ class Rds:
206
206
 
207
207
  不一致なら status が REQUIRE_UPDATE を返し、update() が移行する。
208
208
  """
209
- if self.context.password_strategy == "aws-managed":
209
+ if self.context.password_strategy == "aws-managed": # noqa: S105 戦略名/保存先種別であって secret 値ではない
210
210
  return self._actual_is_managed()
211
211
  # 望む = static
212
212
  if self._actual_is_managed():
@@ -215,13 +215,16 @@ class Rds:
215
215
  return self._store_has_credential(self.context.secret_store)
216
216
 
217
217
  def _get_vpc_stack(self) -> VpcStack:
218
+ if not self.context.vpc:
219
+ raise RuntimeError("vpc context is not configured")
218
220
  return VpcStack(self.context.vpc)
219
221
 
220
222
  def _get_vpc_subnet_ids(self) -> list[str]:
221
223
  vpc_stack = self._get_vpc_stack()
222
224
  output = vpc_stack.output
223
225
  export = vpc_stack.export
224
- assert output, "VPC stack output is not available"
226
+ if not output:
227
+ raise RuntimeError("VPC stack output is not available")
225
228
  prefix = export["private_subnet_"]
226
229
  subnet_ids = []
227
230
  for i in range(1, 20):
@@ -230,14 +233,16 @@ class Rds:
230
233
  subnet_ids.append(output[key])
231
234
  else:
232
235
  break
233
- assert subnet_ids, "No private subnets found in VPC stack"
236
+ if not subnet_ids:
237
+ raise RuntimeError("No private subnets found in VPC stack")
234
238
  return subnet_ids
235
239
 
236
240
  def _get_vpc_id(self) -> str:
237
241
  vpc_stack = self._get_vpc_stack()
238
242
  output = vpc_stack.output
239
243
  export = vpc_stack.export
240
- assert output, "VPC stack output is not available"
244
+ if not output:
245
+ raise RuntimeError("VPC stack output is not available")
241
246
  return output[export["vpc_id"]]
242
247
 
243
248
  def state_info(self):
@@ -260,7 +265,8 @@ class Rds:
260
265
  def deploy_init(self):
261
266
  if not self.context.managed:
262
267
  return
263
- assert self.context.vpc
268
+ if not self.context.vpc:
269
+ raise RuntimeError("vpc context is not configured")
264
270
  vpc_stack = Vpc(self.context.vpc).stack
265
271
  if not self.context.vpc.manage:
266
272
  if vpc_stack.status == "NOEXIST":
@@ -272,6 +278,8 @@ class Rds:
272
278
 
273
279
  def create(self):
274
280
  # VPC スタックの完了を待つ
281
+ if not self.context.vpc:
282
+ raise RuntimeError("vpc context is not configured")
275
283
  if self.context.vpc.manage:
276
284
  Vpc(self.context.vpc).stack.wait_status("COMPLETED")
277
285
  subnet_ids = self._get_vpc_subnet_ids()
@@ -305,7 +313,7 @@ class Rds:
305
313
  )
306
314
  sg_id = sg_res["GroupId"]
307
315
 
308
- static = self.context.password_strategy == "static"
316
+ static = self.context.password_strategy == "static" # noqa: S105 戦略名/保存先種別であって secret 値ではない
309
317
  # static の場合、ここで設定した平文パスワードを後段で secret に保存する。
310
318
  password: str | None = None
311
319
 
@@ -398,7 +406,8 @@ class Rds:
398
406
  # この secret を MasterUserSecret 相当として Lambda へ渡すため、ローテーション
399
407
  # 用 Lambda は付けない (= 自動ローテーションしない)。
400
408
  if static:
401
- assert password is not None
409
+ if password is None:
410
+ raise RuntimeError("password must be set for static credentials")
402
411
  self._store_static_credentials(password)
403
412
 
404
413
  echo.success("Aurora cluster is now available.")
@@ -497,7 +506,7 @@ class Rds:
497
506
 
498
507
  def _migrate_password(self) -> None:
499
508
  """クラスタを望む password_strategy / secret_store に合わせて移行する。"""
500
- if self.context.password_strategy == "aws-managed":
509
+ if self.context.password_strategy == "aws-managed": # noqa: S105 戦略名/保存先種別であって secret 値ではない
501
510
  self._migrate_to_managed()
502
511
  else:
503
512
  self._migrate_to_static()
@@ -533,7 +542,7 @@ class Rds:
533
542
  echo.success("Master password is now pocket-managed (static).")
534
543
  return
535
544
  # クラスタは既に static。store 変更 (sm⇄ssm) を試みる (パスワード変更なし)。
536
- other = "sm" if self.context.secret_store == "ssm" else "ssm"
545
+ other = "sm" if self.context.secret_store == "ssm" else "ssm" # noqa: S105 戦略名/保存先種別であって secret 値ではない
537
546
  existing = self._read_credential_from_store(other)
538
547
  if existing is not None:
539
548
  echo.log(
@@ -601,7 +610,7 @@ class Rds:
601
610
  raise
602
611
 
603
612
  # 5. static: pocket 所有の認証情報を store から削除
604
- if self.context.password_strategy == "static":
613
+ if self.context.password_strategy == "static": # noqa: S105 戦略名/保存先種別であって secret 値ではない
605
614
  self._delete_static_credentials()
606
615
 
607
616
  def _delete_static_credentials(self) -> None:
@@ -53,7 +53,7 @@ class TiDbApi:
53
53
  logger.info("%s %s" % (method, url))
54
54
  if data:
55
55
  logger.debug(json.dumps(data, indent=2))
56
- res = requests.request(method, url, auth=self.auth, json=data)
56
+ res = requests.request(method, url, auth=self.auth, json=data, timeout=30)
57
57
  logger.debug(res.status_code)
58
58
  logger.debug(json.dumps(res.json(), indent=2))
59
59
  if 200 <= res.status_code < 300:
@@ -226,7 +226,7 @@ class TiDb:
226
226
  "public": {
227
227
  "authorizedNetworks": [
228
228
  {
229
- "startIpAddress": "0.0.0.0",
229
+ "startIpAddress": "0.0.0.0", # noqa: S104 TiDB 許可ネットワークの設定値 (socket bind ではない)
230
230
  "endIpAddress": "255.255.255.255",
231
231
  "displayName": "Allow all",
232
232
  }
@@ -34,19 +34,21 @@ class UpstashApi:
34
34
  self.auth = (email, api_key)
35
35
 
36
36
  def get(self, path: str) -> requests.Response:
37
- res = requests.get(f"{self.base_url}/{path}", auth=self.auth)
37
+ res = requests.get(f"{self.base_url}/{path}", auth=self.auth, timeout=30)
38
38
  if 200 <= res.status_code < 300:
39
39
  return res
40
40
  raise RuntimeError("Upstash API error: %s %s" % (res.status_code, res.text))
41
41
 
42
42
  def post(self, path: str, data: dict) -> requests.Response:
43
- res = requests.post(f"{self.base_url}/{path}", auth=self.auth, json=data)
43
+ res = requests.post(
44
+ f"{self.base_url}/{path}", auth=self.auth, json=data, timeout=30
45
+ )
44
46
  if 200 <= res.status_code < 300:
45
47
  return res
46
48
  raise RuntimeError("Upstash API error: %s %s" % (res.status_code, res.text))
47
49
 
48
50
  def delete(self, path: str) -> requests.Response:
49
- res = requests.delete(f"{self.base_url}/{path}", auth=self.auth)
51
+ res = requests.delete(f"{self.base_url}/{path}", auth=self.auth, timeout=30)
50
52
  if 200 <= res.status_code < 300:
51
53
  return res
52
54
  raise RuntimeError("Upstash API error: %s %s" % (res.status_code, res.text))
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "magic-pocket-cli"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "CLI and deploy tools for magic-pocket."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
7
- "magic-pocket>=0.2.0",
7
+ "magic-pocket>=0.2.2",
8
8
  "click>=8.1.7",
9
9
  "jinja2>=3.1.3",
10
10
  "python-on-whales>=0.68.0",