magic-pocket-cli 0.16.0__tar.gz → 0.17.0__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 (69) hide show
  1. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/PKG-INFO +2 -2
  2. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/neon_cli.py +7 -8
  3. magic_pocket_cli-0.17.0/pocket_cli/resources/neon.py +39 -0
  4. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pyproject.toml +2 -2
  5. magic_pocket_cli-0.16.0/pocket_cli/resources/neon.py +0 -392
  6. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/.gitignore +0 -0
  7. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/__init__.py +0 -0
  8. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/__init__.py +0 -0
  9. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/aws_auth.py +0 -0
  10. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/awscontainer_cli.py +0 -0
  11. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/cloudfront_cli.py +0 -0
  12. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/cloudfront_keys_cli.py +0 -0
  13. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/cloudfront_waf_cli.py +0 -0
  14. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/deploy_cli.py +0 -0
  15. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/destroy_cli.py +0 -0
  16. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/dsql_cli.py +0 -0
  17. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/main_cli.py +0 -0
  18. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/migrate_cli.py +0 -0
  19. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/permissions_cli.py +0 -0
  20. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/rds_cli.py +0 -0
  21. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/runtime_config_cli.py +0 -0
  22. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/s3_cli.py +0 -0
  23. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/status_cli.py +0 -0
  24. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/store_url_helper.py +0 -0
  25. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/tidb_cli.py +0 -0
  26. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/upstash_cli.py +0 -0
  27. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/url_helper.py +0 -0
  28. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/vpc_cli.py +0 -0
  29. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/cli/waf_cli.py +0 -0
  30. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/django_cli.py +0 -0
  31. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/mediator.py +0 -0
  32. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/__init__.py +0 -0
  33. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/__init__.py +0 -0
  34. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/builders/__init__.py +0 -0
  35. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/builders/codebuild.py +0 -0
  36. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/builders/depot.py +0 -0
  37. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/builders/docker.py +0 -0
  38. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/builders/dockerignore.py +0 -0
  39. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/cloudformation.py +0 -0
  40. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/ecr.py +0 -0
  41. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/efs.py +0 -0
  42. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/lambdahandler.py +0 -0
  43. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/s3_utils.py +0 -0
  44. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/aws/state.py +0 -0
  45. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/awscontainer.py +0 -0
  46. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/cloudfront.py +0 -0
  47. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/cloudfront_acm.py +0 -0
  48. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/cloudfront_keys.py +0 -0
  49. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/cloudfront_waf.py +0 -0
  50. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/dsql.py +0 -0
  51. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/rds.py +0 -0
  52. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/s3.py +0 -0
  53. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/tidb.py +0 -0
  54. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/upstash.py +0 -0
  55. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/resources/vpc.py +0 -0
  56. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/awscontainer.yaml +0 -0
  57. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cf_function_api_host.js +0 -0
  58. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cf_function_host_redirect_prelude.js +0 -0
  59. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cf_function_spa_auth.js +0 -0
  60. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cf_function_spa_fallback.js +0 -0
  61. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cloudfront.yaml +0 -0
  62. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cloudfront_acm.yaml +0 -0
  63. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cloudfront_keys.yaml +0 -0
  64. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/cloudfront_waf.yaml +0 -0
  65. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/cloudformation/vpc.yaml +0 -0
  66. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/init/django-dotenv.env +0 -0
  67. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/init/django-settings.py +0 -0
  68. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/pocket_cli/templates/init/pocket.Dockerfile +0 -0
  69. {magic_pocket_cli-0.16.0 → magic_pocket_cli-0.17.0}/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.16.0
3
+ Version: 0.17.0
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.13.0
10
+ Requires-Dist: magic-pocket>=0.17.0
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
@@ -6,7 +6,7 @@ from pocket.context import Context
6
6
  from pocket.utils import echo
7
7
  from pocket_cli.cli.store_url_helper import run_store_url
8
8
  from pocket_cli.cli.url_helper import run_get_url
9
- from pocket_cli.resources.neon import Neon
9
+ from pocket_cli.resources.neon import Neon, ensure_url_for_context
10
10
 
11
11
 
12
12
  @click.group()
@@ -83,13 +83,12 @@ def store_url(stage, key, force):
83
83
  """
84
84
 
85
85
  def ensure_and_compute_url(context):
86
- neon = Neon(context.neon)
87
- if not neon.branch:
88
- neon.create_branch(neon.parent_branch)
89
- neon.ensure_role()
90
- neon.ensure_database()
91
- # ensure 後の状態を確実に反映するため fresh instance で URL を算出する。
92
- return Neon(context.neon).database_url
86
+ if not context.neon:
87
+ raise click.ClickException(
88
+ "neon が pocket.toml に宣言されていません (store-url 不可)"
89
+ )
90
+ # ensure + URL 算出は runtime package の共有ヘルパに一本化 (公開 API と同一経路)
91
+ return ensure_url_for_context(context.neon)
93
92
 
94
93
  run_store_url(
95
94
  stage=stage,
@@ -0,0 +1,39 @@
1
+ """Neon リソースの後方互換 re-export。
2
+
3
+ 実装は runtime package の :mod:`pocket.provisioning.neon` に移設済み
4
+ (20260712 feedback: 外部 provisioner が pocket.toml / pocket_cli なしで
5
+ ensure + URL 算出を import 共有できるようにするため)。CLI 側の既存 import
6
+ (`from pocket_cli.resources.neon import Neon` 等) はここで再エクスポートして維持する。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pocket.provisioning.neon import (
12
+ Branch,
13
+ Database,
14
+ Endpoint,
15
+ Neon,
16
+ NeonApi,
17
+ NeonNotFound,
18
+ NeonResourceIsNotReady,
19
+ Project,
20
+ ResourceType,
21
+ Role,
22
+ ensure_and_compute_url,
23
+ ensure_url_for_context,
24
+ )
25
+
26
+ __all__ = [
27
+ "Branch",
28
+ "Database",
29
+ "Endpoint",
30
+ "Neon",
31
+ "NeonApi",
32
+ "NeonNotFound",
33
+ "NeonResourceIsNotReady",
34
+ "Project",
35
+ "ResourceType",
36
+ "Role",
37
+ "ensure_and_compute_url",
38
+ "ensure_url_for_context",
39
+ ]
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "magic-pocket-cli"
3
- version = "0.16.0"
3
+ version = "0.17.0"
4
4
  description = "CLI and deploy tools for magic-pocket."
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
7
- "magic-pocket>=0.13.0",
7
+ "magic-pocket>=0.17.0",
8
8
  "click>=8.1.7",
9
9
  "jinja2>=3.1.3",
10
10
  "python-on-whales>=0.68.0",
@@ -1,392 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import logging
5
- import os
6
- import re
7
- import time
8
- from functools import cached_property
9
- from typing import TYPE_CHECKING, Literal
10
-
11
- import requests
12
- from pydantic import BaseModel
13
-
14
- from pocket.resources.base import ResourceStatus
15
-
16
- if TYPE_CHECKING:
17
- from pocket.context import NeonContext
18
-
19
- logging.basicConfig()
20
- logger = logging.getLogger(__name__)
21
- logger.setLevel(level=os.getenv("POCKET_LOGGER_LEVEL", "WARNING").upper())
22
-
23
- ResourceType = Literal["branches", "databases", "endpoints", "roles"]
24
-
25
-
26
- class NeonResourceIsNotReady(Exception):
27
- pass
28
-
29
-
30
- class NeonNotFound(Exception):
31
- """Neon API が 404 を返したことを示す例外。
32
-
33
- `branch` / `role` などの個別取得で対象が存在しない場合にこの例外を投げる。
34
- 呼び出し側は存在しないことを明示的に判定できる。
35
- """
36
-
37
- pass
38
-
39
-
40
- class Project(BaseModel):
41
- id: str
42
- name: str
43
-
44
-
45
- class Branch(BaseModel):
46
- id: str
47
- name: str
48
-
49
-
50
- class Database(BaseModel):
51
- name: str
52
- owner_name: str
53
-
54
-
55
- class Endpoint(BaseModel):
56
- id: str
57
- host: str
58
- autoscaling_limit_min_cu: float
59
- autoscaling_limit_max_cu: float
60
- type: Literal["read_write", "read_only"]
61
-
62
-
63
- class Role(BaseModel):
64
- name: str
65
- password: str | None = None
66
-
67
-
68
- class NeonApi:
69
- endpoint = "https://console.neon.tech/api/v2/"
70
-
71
- def __init__(self, key) -> None:
72
- self.key = key
73
-
74
- @property
75
- def header(self):
76
- return {
77
- "Accept": "application/json",
78
- "Authorization": "Bearer %s" % self.key,
79
- }
80
-
81
- def get(self, path):
82
- logger.info("GET %s" % self.endpoint + path)
83
- res = requests.get(self.endpoint + path, headers=self.header, timeout=30)
84
- logger.debug(res.status_code)
85
- logger.debug(json.dumps(res.json(), indent=2))
86
- if 200 <= res.status_code < 300:
87
- return res
88
- if res.status_code == 404:
89
- raise NeonNotFound("%s: %s" % (res.status_code, res.json()["message"]))
90
- if res.status_code == 401:
91
- print("Used API key: %s" % (self.key[:5] + "..." + self.key[-5:]))
92
- print("API key length: %s" % len(self.key))
93
- raise Exception("%s: %s" % (res.status_code, res.json()["message"]))
94
-
95
- def post(self, path, data=None):
96
- logger.warning("POST %s" % self.endpoint + path)
97
- logger.debug(json.dumps(data, indent=2))
98
- res = requests.post(
99
- self.endpoint + path, headers=self.header, json=data, timeout=30
100
- )
101
- logger.debug(res.status_code)
102
- logger.debug(json.dumps(res.json(), indent=2))
103
- if 200 <= res.status_code < 300:
104
- time.sleep(2)
105
- return res
106
- if res.status_code == 401:
107
- print("Used API key: %s" % (self.key[:5] + "..." + self.key[-5:]))
108
- print("API key length: %s" % len(self.key))
109
- raise Exception("%s: %s" % (res.status_code, res.json()["message"]))
110
-
111
- def delete(self, path, data=None):
112
- logger.warning("DELETE %s" % self.endpoint + path)
113
- logger.debug(json.dumps(data, indent=2))
114
- res = requests.delete(
115
- self.endpoint + path, headers=self.header, json=data, timeout=30
116
- )
117
- logger.debug(res.status_code)
118
- logger.debug(json.dumps(res.json(), indent=2))
119
- if 200 <= res.status_code < 300:
120
- time.sleep(2)
121
- return res
122
- raise Exception("%s: %s" % (res.status_code, res.json()["message"]))
123
-
124
- def projects_url(self):
125
- return self.endpoint + "projects"
126
-
127
-
128
- class Neon:
129
- context: NeonContext
130
-
131
- def __init__(self, context: NeonContext) -> None:
132
- self.context = context
133
-
134
- def get_resource_path(self, resource_type: ResourceType) -> str:
135
- requirements = {
136
- "branches": ["project"],
137
- "databases": ["project", "branch"],
138
- "endpoints": ["project"],
139
- "roles": ["project", "branch"],
140
- }
141
- path_templates = {
142
- "branches": "projects/%(project_id)s/branches",
143
- "databases": "projects/%(project_id)s/branches/%(branch_id)s/databases",
144
- "endpoints": "projects/%(project_id)s/endpoints",
145
- "roles": "projects/%(project_id)s/branches/%(branch_id)s/roles",
146
- }
147
- path_context = {}
148
- for requirement in requirements[resource_type]:
149
- if not getattr(self, requirement):
150
- raise Exception("%s not found" % requirement)
151
- path_context[requirement + "_id"] = getattr(self, requirement).id
152
- return path_templates[resource_type] % path_context
153
-
154
- def construct_path(
155
- self, resource_type: ResourceType, resource_id: str | None = None
156
- ):
157
- path = self.get_resource_path(resource_type)
158
- if resource_id:
159
- path += "/" + resource_id
160
- return path
161
-
162
- def get(self, resource_type: ResourceType, resource_id: str | None = None):
163
- path = self.construct_path(resource_type, resource_id)
164
- return self.api.get(path)
165
-
166
- def post(
167
- self, resource_type: ResourceType, resource_id: str | None = None, data=None
168
- ):
169
- path = self.construct_path(resource_type, resource_id)
170
- return self.api.post(path, data)
171
-
172
- def delete(
173
- self, resource_type: ResourceType, resource_id: str | None = None, data=None
174
- ):
175
- path = self.construct_path(resource_type, resource_id)
176
- return self.api.delete(path, data)
177
-
178
- @property
179
- def api(self):
180
- return NeonApi(self.context.api_key)
181
-
182
- @cached_property
183
- def role(self) -> Role | None:
184
- if not self.branch:
185
- return None
186
- try:
187
- res = self.get("roles", self.context.role_name)
188
- except NeonNotFound:
189
- return None
190
- return Role(**res.json()["role"])
191
-
192
- @cached_property
193
- def project(self) -> Project:
194
- """project_nameからNeonプロジェクトを解決する。
195
-
196
- 組織キー: GET /projects で一覧取得し name で検索。
197
- プロジェクトスコープキー: エラーから project_id を取得し直接アクセス。
198
- """
199
- res = requests.get(
200
- self.api.endpoint + "projects", headers=self.api.header, timeout=30
201
- )
202
- if 200 <= res.status_code < 300:
203
- for p in res.json().get("projects", []):
204
- if p["name"] == self.context.project_name:
205
- return Project(**p)
206
- raise ValueError(f"Neon project '{self.context.project_name}' not found")
207
-
208
- # プロジェクトスコープキー: エラーから project_id をパース
209
- message = res.json().get("message", "")
210
- match = re.search(r'subject_project_id:"([^"]+)"', message)
211
- if not match:
212
- raise ValueError(
213
- f"Failed to resolve Neon project: {res.status_code}: {message}"
214
- )
215
- project_id = match.group(1)
216
- project_res = self.api.get(f"projects/{project_id}")
217
- project_data = project_res.json()["project"]
218
- if project_data["name"] != self.context.project_name:
219
- raise ValueError(
220
- f"Neon project name mismatch: "
221
- f"config='{self.context.project_name}', "
222
- f"actual='{project_data['name']}'"
223
- )
224
- return Project(id=project_data["id"], name=project_data["name"])
225
-
226
- @cached_property
227
- def branch(self) -> Branch | None:
228
- if self.project:
229
- for branch in self.get("branches").json()["branches"]:
230
- if branch["name"] == self.context.branch_name:
231
- return Branch(**branch)
232
-
233
- @cached_property
234
- def parent_branch(self) -> Branch | None:
235
- """branch を新規作成する際の親ブランチ。
236
-
237
- context.parent_branch_name が未指定なら None (= create_branch が parent_id を
238
- 送らず Neon の default ブランチから分岐)。指定があるのに project 内に該当
239
- ブランチが無い場合はエラー (黙って default 分岐すると事故になるため)。
240
- """
241
- if not self.context.parent_branch_name:
242
- return None
243
- if self.project:
244
- for branch in self.get("branches").json()["branches"]:
245
- if branch["name"] == self.context.parent_branch_name:
246
- return Branch(**branch)
247
- raise ValueError(
248
- f"Neon parent branch '{self.context.parent_branch_name}' not found "
249
- f"in project '{self.context.project_name}'"
250
- )
251
-
252
- @cached_property
253
- def database(self) -> Database | None:
254
- if self.branch:
255
- for database in self.get("databases").json()["databases"]:
256
- if database["name"] == self.context.name:
257
- return Database(**database)
258
-
259
- @cached_property
260
- def endpoint(self) -> Endpoint | None:
261
- if self.branch:
262
- for endpoint in self.get("endpoints").json()["endpoints"]:
263
- if endpoint["branch_id"] == self.branch.id:
264
- return Endpoint(**endpoint)
265
-
266
- @property
267
- def database_url(self):
268
- if self.role is None or self.endpoint is None:
269
- raise NeonResourceIsNotReady("Create role and endpoint first")
270
- if self.role.password is None:
271
- self.set_role_password()
272
- return "postgres://%s:%s@%s:5432/%s?sslmode=require" % (
273
- self.context.role_name,
274
- self.role.password,
275
- self.endpoint.host,
276
- self.context.name,
277
- )
278
-
279
- @property
280
- def status(self) -> ResourceStatus:
281
- # provisioning="command" の Neon は get_resources で除外されるため、ここに
282
- # 到達するのは deploy が Neon を管理する provisioning="deploy" の場合のみ。
283
- if self.working:
284
- return "COMPLETED"
285
- return "NOEXIST"
286
-
287
- @property
288
- def working(self):
289
- check = [self.branch, self.database, self.endpoint, self.role]
290
- logger.info(str(check))
291
- return all(check)
292
-
293
- @property
294
- def description(self):
295
- return "Create Neon branch, database, role, and endpoint"
296
-
297
- def create_new(self):
298
- self.create()
299
- self.reset_database()
300
-
301
- def state_info(self):
302
- return {
303
- "neon": {
304
- "project_name": self.context.project_name,
305
- "branch_name": self.context.branch_name,
306
- }
307
- }
308
-
309
- def deploy_init(self):
310
- pass
311
-
312
- def create(self):
313
- # branch が既に存在するなら (Neon project 作成時に自動生成される default main
314
- # を含む) その上に role/database を ensure するだけにし branch 作成はスキップ。
315
- # parent_branch の解決も branch 不在時のみ行う (存在するのに親を要求して誤って
316
- # ValueError にしないため)。これで default main を使う stage の初回 deploy が
317
- # 409 (branch already exists) にならず、既存 branch への db/role bootstrap も
318
- # deploy で完結する。
319
- if self.branch is None:
320
- # parent_branch_name 指定時はその親から分岐。未指定なら parent_branch=None
321
- # で従来通り Neon default ブランチから分岐する。
322
- self.create_branch(self.parent_branch)
323
- self.ensure_role()
324
- self.ensure_database()
325
-
326
- def create_branch(self, base_branch: Branch | None = None):
327
- # 冪等化: context.branch_name の branch が既に存在するなら POST /branches は
328
- # 409 (branch already exists) になるため作成をスキップする。branch_out /
329
- # store_url の呼び出し側も branch 不在を確認済だが、直接呼ばれた場合の防御。
330
- if self.branch is not None:
331
- return
332
- # branch/endpoint の cached_property を無効化し、branch 作成後の再取得を
333
- # 強制する。endpoint はこの経路で一度も access していない (cache 未生成) ため
334
- # 素の `del` は AttributeError になる。access 有無に依らず安全な pop で消す。
335
- self.__dict__.pop("branch", None)
336
- self.__dict__.pop("endpoint", None)
337
- data = {
338
- "branch": {
339
- "name": self.context.branch_name,
340
- },
341
- "endpoints": [{"type": "read_write"}],
342
- }
343
- if base_branch:
344
- data["branch"]["parent_id"] = base_branch.id
345
- self.post("branches", data=data)
346
-
347
- def delete_branch(self):
348
- if not self.endpoint or not self.branch:
349
- raise Exception("Branch or endpoint not found. Something is wrong.")
350
- self.delete("endpoints", self.endpoint.id)
351
- self.delete("branches", self.branch.id)
352
-
353
- def ensure_database(self):
354
- if self.database is None:
355
- self.create_database()
356
-
357
- def create_database(self):
358
- if self.database is None:
359
- del self.database
360
- data = {
361
- "database": {
362
- "name": self.context.name,
363
- "owner_name": self.context.role_name,
364
- }
365
- }
366
- self.post("databases", data=data)
367
-
368
- def reset_database(self):
369
- if self.database:
370
- self.delete("databases", self.context.name)
371
- self.create_database()
372
-
373
- def create_role(self):
374
- if self.role is None:
375
- del self.role
376
- data = {"role": {"name": self.context.role_name}}
377
- self.post("roles", data=data)
378
-
379
- def set_role_password(self):
380
- if self.role is None:
381
- raise Exception("Create role first")
382
- if self.role.password is None:
383
- self.role.password = self.get(
384
- "roles", self.role.name + "/reveal_password"
385
- ).json()["password"]
386
-
387
- def ensure_role(self):
388
- if self.role is None:
389
- del self.role
390
- data = {"role": {"name": self.context.role_name}}
391
- self.post("roles", data=data)
392
- self.set_role_password()