cacholong-cloud-cli 1.13.2__tar.gz → 1.14.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 (55) hide show
  1. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/PKG-INFO +1 -1
  2. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/cli.py +6 -0
  3. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/dns_zones.py +0 -5
  4. cacholong_cloud_cli-1.14.1/cacholong_cli/commands/service_bundles.py +905 -0
  5. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/common.py +20 -3
  6. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/__init__.py +2 -0
  7. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/api_schema.py +62 -1
  8. cacholong_cloud_cli-1.14.1/cacholong_sdk/service_bundles.py +14 -0
  9. cacholong_cloud_cli-1.14.1/cacholong_sdk/services.py +14 -0
  10. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/pyproject.toml +1 -1
  11. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/README.md +0 -0
  12. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/AsyncTyper.py +0 -0
  13. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/__init__.py +0 -0
  14. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/accounts.py +0 -0
  15. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/addresses.py +0 -0
  16. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/companies.py +0 -0
  17. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/dns_records.py +0 -0
  18. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/dns_templates.py +0 -0
  19. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/dns_zone_restore_points.py +0 -0
  20. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/mail_address_aliases.py +0 -0
  21. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/mail_addresses.py +0 -0
  22. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/mail_domains.py +0 -0
  23. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/mail_forwards.py +0 -0
  24. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/products.py +0 -0
  25. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/purchases.py +0 -0
  26. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/commands/users.py +0 -0
  27. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_cli/connection.py +0 -0
  28. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/accounts.py +0 -0
  29. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/addresses.py +0 -0
  30. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/common.py +0 -0
  31. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/companies.py +0 -0
  32. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/connection.py +0 -0
  33. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/dns_records.py +0 -0
  34. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/dns_templates.py +0 -0
  35. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/dns_zone_restore_points.py +0 -0
  36. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/dns_zones.py +0 -0
  37. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/exception.py +0 -0
  38. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/mail_address_aliases.py +0 -0
  39. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/mail_addresses.py +0 -0
  40. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/mail_domains.py +0 -0
  41. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/mail_forwards.py +0 -0
  42. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/products.py +0 -0
  43. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/purchases.py +0 -0
  44. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/service_mails.py +0 -0
  45. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/user_roles.py +0 -0
  46. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/cacholong_sdk/users.py +0 -0
  47. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/__init__.py +0 -0
  48. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/common.py +0 -0
  49. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/document.py +0 -0
  50. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/exceptions.py +0 -0
  51. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/filter.py +0 -0
  52. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/objects.py +0 -0
  53. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/relationships.py +0 -0
  54. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/resourceobject.py +0 -0
  55. {cacholong_cloud_cli-1.13.2 → cacholong_cloud_cli-1.14.1}/jsonapi-client/src/jsonapi_client/session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cacholong-cloud-cli
3
- Version: 1.13.2
3
+ Version: 1.14.1
4
4
  Summary: Cacholong CLI, manage cloud environments at Cacholong!
5
5
  License: MIT
6
6
  Author: Matthijs Möhlmann
@@ -18,6 +18,7 @@ from cacholong_cli.commands import (
18
18
  mail_forwards,
19
19
  products,
20
20
  purchases,
21
+ service_bundles,
21
22
  users,
22
23
  )
23
24
  from cacholong_cli.common import OutputFormat
@@ -64,6 +65,11 @@ app.add_typer(mail_domains.app, name="mail-domains", help="Manage mail domains")
64
65
  app.add_typer(mail_forwards.app, name="mail-forwards", help="Manage mail forwards")
65
66
  app.add_typer(products.app, name="products", help="Manage products")
66
67
  app.add_typer(purchases.app, name="purchases", help="Manage purchases")
68
+ app.add_typer(
69
+ service_bundles.app,
70
+ name="service-bundles",
71
+ help='A service bundle is a named set of services that together deliver a product. It works as the "recipe": it states which PHP versions, which nginx, which MySQL and so on are used, and therefore on which servers or at which provider a product ends up.',
72
+ )
67
73
  app.add_typer(users.app, name="users", help="Manage users")
68
74
 
69
75
 
@@ -346,9 +346,6 @@ async def create(
346
346
  async def update(
347
347
  ctx: typer.Context,
348
348
  dns_zone_id: Annotated[UUID, typer.Argument(help="Resource ID to update")],
349
- domain: Annotated[
350
- Optional[str], typer.Option(help="The domain name for this DNS zone")
351
- ] = None,
352
349
  active: Annotated[
353
350
  Optional[bool],
354
351
  typer.Option(help="Enable or disable active DNS resolution for this zone"),
@@ -358,8 +355,6 @@ async def update(
358
355
  ctrl = DnsZone(conn, api_schema)
359
356
  model = await ctrl.fetch(dns_zone_id)
360
357
 
361
- if domain is not None:
362
- model["domain"] = domain
363
358
  if active is not None:
364
359
  model["active"] = active
365
360
 
@@ -0,0 +1,905 @@
1
+ import asyncio
2
+ import typer
3
+ from typing import List, Optional, Dict, Any, Union, Type
4
+ from typing_extensions import Annotated
5
+ from rich.table import Column
6
+ from uuid import UUID
7
+ from datetime import datetime, date, timezone
8
+ from cacholong_cli.AsyncTyper import AsyncTyper
9
+ from cacholong_cli.common import (
10
+ list_resources,
11
+ list_relation_resources,
12
+ show_resource,
13
+ TableDef,
14
+ )
15
+ from cacholong_cli.connection import Connection
16
+ from cacholong_sdk import Filter, Inclusion, ResourceTuple
17
+ from cacholong_sdk import ServiceBundle, ServiceBundleModel
18
+ from cacholong_sdk.api_schema import api_schema
19
+
20
+ # Create typer object
21
+ app = AsyncTyper()
22
+
23
+
24
+ @app.async_command(help="Get service bundles representing collections of services")
25
+ async def list(
26
+ ctx: typer.Context,
27
+ name: Annotated[
28
+ Optional[str],
29
+ typer.Option(help="Service bundle name (must be unique within account)"),
30
+ ] = None,
31
+ description: Annotated[
32
+ Optional[str], typer.Option(help="Optional description for this service bundle")
33
+ ] = None,
34
+ account: Annotated[
35
+ Optional[UUID], typer.Option(help="Account owner (null for a global bundle)")
36
+ ] = None,
37
+ created_at: Annotated[
38
+ Optional[datetime],
39
+ typer.Option(help="Creation timestamp in ISO 8601 format (exact match)"),
40
+ ] = None,
41
+ created_at_gte: Annotated[
42
+ Optional[datetime],
43
+ typer.Option(
44
+ help="Creation timestamp in ISO 8601 format (greater than or equal)"
45
+ ),
46
+ ] = None,
47
+ created_at_lte: Annotated[
48
+ Optional[datetime],
49
+ typer.Option(help="Creation timestamp in ISO 8601 format (less than or equal)"),
50
+ ] = None,
51
+ created_at_gt: Annotated[
52
+ Optional[datetime],
53
+ typer.Option(help="Creation timestamp in ISO 8601 format (greater than)"),
54
+ ] = None,
55
+ created_at_lt: Annotated[
56
+ Optional[datetime],
57
+ typer.Option(help="Creation timestamp in ISO 8601 format (less than)"),
58
+ ] = None,
59
+ updated_at: Annotated[
60
+ Optional[datetime],
61
+ typer.Option(help="Last update timestamp in ISO 8601 format (exact match)"),
62
+ ] = None,
63
+ updated_at_gte: Annotated[
64
+ Optional[datetime],
65
+ typer.Option(
66
+ help="Last update timestamp in ISO 8601 format (greater than or equal)"
67
+ ),
68
+ ] = None,
69
+ updated_at_lte: Annotated[
70
+ Optional[datetime],
71
+ typer.Option(
72
+ help="Last update timestamp in ISO 8601 format (less than or equal)"
73
+ ),
74
+ ] = None,
75
+ updated_at_gt: Annotated[
76
+ Optional[datetime],
77
+ typer.Option(help="Last update timestamp in ISO 8601 format (greater than)"),
78
+ ] = None,
79
+ updated_at_lt: Annotated[
80
+ Optional[datetime],
81
+ typer.Option(help="Last update timestamp in ISO 8601 format (less than)"),
82
+ ] = None,
83
+ ):
84
+
85
+ # Build modifier
86
+ modifier = []
87
+
88
+ if name is not None:
89
+ modifier.append(Filter(name=name))
90
+
91
+ if description is not None:
92
+ modifier.append(Filter(description=description))
93
+
94
+ if account is not None:
95
+ modifier.append(Filter(account=str(account)))
96
+
97
+ if created_at is not None:
98
+ modifier.append(
99
+ Filter(
100
+ query_str="filter[created_at]="
101
+ + created_at.astimezone(timezone.utc)
102
+ .replace(tzinfo=None)
103
+ .isoformat(timespec="milliseconds")
104
+ + "Z"
105
+ )
106
+ )
107
+ if created_at_gte is not None:
108
+ modifier.append(
109
+ Filter(
110
+ query_str="filter[created_at_gte]="
111
+ + created_at_gte.astimezone(timezone.utc)
112
+ .replace(tzinfo=None)
113
+ .isoformat(timespec="milliseconds")
114
+ + "Z"
115
+ )
116
+ )
117
+ if created_at_lte is not None:
118
+ modifier.append(
119
+ Filter(
120
+ query_str="filter[created_at_lte]="
121
+ + created_at_lte.astimezone(timezone.utc)
122
+ .replace(tzinfo=None)
123
+ .isoformat(timespec="milliseconds")
124
+ + "Z"
125
+ )
126
+ )
127
+ if created_at_gt is not None:
128
+ modifier.append(
129
+ Filter(
130
+ query_str="filter[created_at_gt]="
131
+ + created_at_gt.astimezone(timezone.utc)
132
+ .replace(tzinfo=None)
133
+ .isoformat(timespec="milliseconds")
134
+ + "Z"
135
+ )
136
+ )
137
+ if created_at_lt is not None:
138
+ modifier.append(
139
+ Filter(
140
+ query_str="filter[created_at_lt]="
141
+ + created_at_lt.astimezone(timezone.utc)
142
+ .replace(tzinfo=None)
143
+ .isoformat(timespec="milliseconds")
144
+ + "Z"
145
+ )
146
+ )
147
+
148
+ if updated_at is not None:
149
+ modifier.append(
150
+ Filter(
151
+ query_str="filter[updated_at]="
152
+ + updated_at.astimezone(timezone.utc)
153
+ .replace(tzinfo=None)
154
+ .isoformat(timespec="milliseconds")
155
+ + "Z"
156
+ )
157
+ )
158
+ if updated_at_gte is not None:
159
+ modifier.append(
160
+ Filter(
161
+ query_str="filter[updated_at_gte]="
162
+ + updated_at_gte.astimezone(timezone.utc)
163
+ .replace(tzinfo=None)
164
+ .isoformat(timespec="milliseconds")
165
+ + "Z"
166
+ )
167
+ )
168
+ if updated_at_lte is not None:
169
+ modifier.append(
170
+ Filter(
171
+ query_str="filter[updated_at_lte]="
172
+ + updated_at_lte.astimezone(timezone.utc)
173
+ .replace(tzinfo=None)
174
+ .isoformat(timespec="milliseconds")
175
+ + "Z"
176
+ )
177
+ )
178
+ if updated_at_gt is not None:
179
+ modifier.append(
180
+ Filter(
181
+ query_str="filter[updated_at_gt]="
182
+ + updated_at_gt.astimezone(timezone.utc)
183
+ .replace(tzinfo=None)
184
+ .isoformat(timespec="milliseconds")
185
+ + "Z"
186
+ )
187
+ )
188
+ if updated_at_lt is not None:
189
+ modifier.append(
190
+ Filter(
191
+ query_str="filter[updated_at_lt]="
192
+ + updated_at_lt.astimezone(timezone.utc)
193
+ .replace(tzinfo=None)
194
+ .isoformat(timespec="milliseconds")
195
+ + "Z"
196
+ )
197
+ )
198
+
199
+ modifier.append(Inclusion("account"))
200
+
201
+ # Table definition
202
+ tabledef: TableDef = [
203
+ {"header": Column("Id", no_wrap=True), "column": "id"},
204
+ {
205
+ "header": "Account",
206
+ "column": "account",
207
+ "column_kebab": "account",
208
+ "nested_column": "name",
209
+ },
210
+ {"header": "Name", "column": "name"},
211
+ {"header": "Created", "column": "created_at"},
212
+ {"header": "Updated", "column": "updated_at"},
213
+ ]
214
+
215
+ async with Connection() as conn:
216
+ await list_resources(ctx, ServiceBundle(conn, api_schema), tabledef, modifier)
217
+
218
+
219
+ @app.async_command(help="Get service bundle details by ID")
220
+ async def show(
221
+ ctx: typer.Context,
222
+ service_bundle_id: Annotated[UUID, typer.Argument(help="Resource ID to show")],
223
+ ):
224
+ # Show resource
225
+ async with Connection() as conn:
226
+ ctrl = ServiceBundle(conn, api_schema)
227
+ model = await ctrl.fetch(service_bundle_id)
228
+
229
+ show_resource(ctx, model)
230
+
231
+
232
+ @app.async_command(
233
+ name="show-account", help="Get the account associated with a service bundle"
234
+ )
235
+ async def show_account(
236
+ ctx: typer.Context,
237
+ service_bundle_id: Annotated[UUID, typer.Argument(help="Parent resource ID")],
238
+ ):
239
+ """Get the account associated with a service bundle"""
240
+ async with Connection() as conn:
241
+ # Fetch parent resource
242
+ parent_ctrl = ServiceBundle(conn, api_schema)
243
+ parent_model = await parent_ctrl.fetch(service_bundle_id)
244
+
245
+ # Fetch and show related resource
246
+ await parent_model["account"].fetch()
247
+ related = parent_model["account"].resource
248
+
249
+ show_resource(ctx, related)
250
+
251
+
252
+ @app.async_command(name="list-services", help="List services for a service bundle")
253
+ async def list_services(
254
+ ctx: typer.Context,
255
+ service_bundle_id: Annotated[UUID, typer.Argument(help="Parent resource ID")],
256
+ category: Annotated[
257
+ Optional[str],
258
+ typer.Option(
259
+ help="Service category (apache, dns, domain, firewall, mysql, nginx, osuser, php, postgresql, shell, ssl)"
260
+ ),
261
+ ] = None,
262
+ name: Annotated[
263
+ Optional[str], typer.Option(help="Service name (descriptive identifier)")
264
+ ] = None,
265
+ description: Annotated[
266
+ Optional[str], typer.Option(help="Detailed description of the service")
267
+ ] = None,
268
+ version: Annotated[
269
+ Optional[str], typer.Option(help="Service version number")
270
+ ] = None,
271
+ deprecated_at: Annotated[
272
+ Optional[datetime],
273
+ typer.Option(
274
+ help="Planned deprecation date in ISO 8601 format (null if not scheduled) (exact match)"
275
+ ),
276
+ ] = None,
277
+ deprecated_at_gte: Annotated[
278
+ Optional[datetime],
279
+ typer.Option(
280
+ help="Planned deprecation date in ISO 8601 format (null if not scheduled) (greater than or equal)"
281
+ ),
282
+ ] = None,
283
+ deprecated_at_lte: Annotated[
284
+ Optional[datetime],
285
+ typer.Option(
286
+ help="Planned deprecation date in ISO 8601 format (null if not scheduled) (less than or equal)"
287
+ ),
288
+ ] = None,
289
+ deprecated_at_gt: Annotated[
290
+ Optional[datetime],
291
+ typer.Option(
292
+ help="Planned deprecation date in ISO 8601 format (null if not scheduled) (greater than)"
293
+ ),
294
+ ] = None,
295
+ deprecated_at_lt: Annotated[
296
+ Optional[datetime],
297
+ typer.Option(
298
+ help="Planned deprecation date in ISO 8601 format (null if not scheduled) (less than)"
299
+ ),
300
+ ] = None,
301
+ created_at: Annotated[
302
+ Optional[datetime],
303
+ typer.Option(help="Creation timestamp in ISO 8601 format (exact match)"),
304
+ ] = None,
305
+ created_at_gte: Annotated[
306
+ Optional[datetime],
307
+ typer.Option(
308
+ help="Creation timestamp in ISO 8601 format (greater than or equal)"
309
+ ),
310
+ ] = None,
311
+ created_at_lte: Annotated[
312
+ Optional[datetime],
313
+ typer.Option(help="Creation timestamp in ISO 8601 format (less than or equal)"),
314
+ ] = None,
315
+ created_at_gt: Annotated[
316
+ Optional[datetime],
317
+ typer.Option(help="Creation timestamp in ISO 8601 format (greater than)"),
318
+ ] = None,
319
+ created_at_lt: Annotated[
320
+ Optional[datetime],
321
+ typer.Option(help="Creation timestamp in ISO 8601 format (less than)"),
322
+ ] = None,
323
+ updated_at: Annotated[
324
+ Optional[datetime],
325
+ typer.Option(help="Last update timestamp in ISO 8601 format (exact match)"),
326
+ ] = None,
327
+ updated_at_gte: Annotated[
328
+ Optional[datetime],
329
+ typer.Option(
330
+ help="Last update timestamp in ISO 8601 format (greater than or equal)"
331
+ ),
332
+ ] = None,
333
+ updated_at_lte: Annotated[
334
+ Optional[datetime],
335
+ typer.Option(
336
+ help="Last update timestamp in ISO 8601 format (less than or equal)"
337
+ ),
338
+ ] = None,
339
+ updated_at_gt: Annotated[
340
+ Optional[datetime],
341
+ typer.Option(help="Last update timestamp in ISO 8601 format (greater than)"),
342
+ ] = None,
343
+ updated_at_lt: Annotated[
344
+ Optional[datetime],
345
+ typer.Option(help="Last update timestamp in ISO 8601 format (less than)"),
346
+ ] = None,
347
+ ):
348
+ """List services for a service bundle"""
349
+ async with Connection() as conn:
350
+ # Fetch parent resource
351
+ parent_ctrl = ServiceBundle(conn, api_schema)
352
+ parent_model = await parent_ctrl.fetch(service_bundle_id)
353
+
354
+ # Build modifier for filtering and includes
355
+ modifier = []
356
+
357
+ if category is not None:
358
+ modifier.append(Filter(category=category))
359
+
360
+ if name is not None:
361
+ modifier.append(Filter(name=name))
362
+
363
+ if description is not None:
364
+ modifier.append(Filter(description=description))
365
+
366
+ if version is not None:
367
+ modifier.append(Filter(version=version))
368
+
369
+ if deprecated_at is not None:
370
+ modifier.append(
371
+ Filter(
372
+ query_str="filter[deprecated_at]="
373
+ + deprecated_at.astimezone(timezone.utc)
374
+ .replace(tzinfo=None)
375
+ .isoformat(timespec="milliseconds")
376
+ + "Z"
377
+ )
378
+ )
379
+ if deprecated_at_gte is not None:
380
+ modifier.append(
381
+ Filter(
382
+ query_str="filter[deprecated_at_gte]="
383
+ + deprecated_at_gte.astimezone(timezone.utc)
384
+ .replace(tzinfo=None)
385
+ .isoformat(timespec="milliseconds")
386
+ + "Z"
387
+ )
388
+ )
389
+ if deprecated_at_lte is not None:
390
+ modifier.append(
391
+ Filter(
392
+ query_str="filter[deprecated_at_lte]="
393
+ + deprecated_at_lte.astimezone(timezone.utc)
394
+ .replace(tzinfo=None)
395
+ .isoformat(timespec="milliseconds")
396
+ + "Z"
397
+ )
398
+ )
399
+ if deprecated_at_gt is not None:
400
+ modifier.append(
401
+ Filter(
402
+ query_str="filter[deprecated_at_gt]="
403
+ + deprecated_at_gt.astimezone(timezone.utc)
404
+ .replace(tzinfo=None)
405
+ .isoformat(timespec="milliseconds")
406
+ + "Z"
407
+ )
408
+ )
409
+ if deprecated_at_lt is not None:
410
+ modifier.append(
411
+ Filter(
412
+ query_str="filter[deprecated_at_lt]="
413
+ + deprecated_at_lt.astimezone(timezone.utc)
414
+ .replace(tzinfo=None)
415
+ .isoformat(timespec="milliseconds")
416
+ + "Z"
417
+ )
418
+ )
419
+
420
+ if created_at is not None:
421
+ modifier.append(
422
+ Filter(
423
+ query_str="filter[created_at]="
424
+ + created_at.astimezone(timezone.utc)
425
+ .replace(tzinfo=None)
426
+ .isoformat(timespec="milliseconds")
427
+ + "Z"
428
+ )
429
+ )
430
+ if created_at_gte is not None:
431
+ modifier.append(
432
+ Filter(
433
+ query_str="filter[created_at_gte]="
434
+ + created_at_gte.astimezone(timezone.utc)
435
+ .replace(tzinfo=None)
436
+ .isoformat(timespec="milliseconds")
437
+ + "Z"
438
+ )
439
+ )
440
+ if created_at_lte is not None:
441
+ modifier.append(
442
+ Filter(
443
+ query_str="filter[created_at_lte]="
444
+ + created_at_lte.astimezone(timezone.utc)
445
+ .replace(tzinfo=None)
446
+ .isoformat(timespec="milliseconds")
447
+ + "Z"
448
+ )
449
+ )
450
+ if created_at_gt is not None:
451
+ modifier.append(
452
+ Filter(
453
+ query_str="filter[created_at_gt]="
454
+ + created_at_gt.astimezone(timezone.utc)
455
+ .replace(tzinfo=None)
456
+ .isoformat(timespec="milliseconds")
457
+ + "Z"
458
+ )
459
+ )
460
+ if created_at_lt is not None:
461
+ modifier.append(
462
+ Filter(
463
+ query_str="filter[created_at_lt]="
464
+ + created_at_lt.astimezone(timezone.utc)
465
+ .replace(tzinfo=None)
466
+ .isoformat(timespec="milliseconds")
467
+ + "Z"
468
+ )
469
+ )
470
+
471
+ if updated_at is not None:
472
+ modifier.append(
473
+ Filter(
474
+ query_str="filter[updated_at]="
475
+ + updated_at.astimezone(timezone.utc)
476
+ .replace(tzinfo=None)
477
+ .isoformat(timespec="milliseconds")
478
+ + "Z"
479
+ )
480
+ )
481
+ if updated_at_gte is not None:
482
+ modifier.append(
483
+ Filter(
484
+ query_str="filter[updated_at_gte]="
485
+ + updated_at_gte.astimezone(timezone.utc)
486
+ .replace(tzinfo=None)
487
+ .isoformat(timespec="milliseconds")
488
+ + "Z"
489
+ )
490
+ )
491
+ if updated_at_lte is not None:
492
+ modifier.append(
493
+ Filter(
494
+ query_str="filter[updated_at_lte]="
495
+ + updated_at_lte.astimezone(timezone.utc)
496
+ .replace(tzinfo=None)
497
+ .isoformat(timespec="milliseconds")
498
+ + "Z"
499
+ )
500
+ )
501
+ if updated_at_gt is not None:
502
+ modifier.append(
503
+ Filter(
504
+ query_str="filter[updated_at_gt]="
505
+ + updated_at_gt.astimezone(timezone.utc)
506
+ .replace(tzinfo=None)
507
+ .isoformat(timespec="milliseconds")
508
+ + "Z"
509
+ )
510
+ )
511
+ if updated_at_lt is not None:
512
+ modifier.append(
513
+ Filter(
514
+ query_str="filter[updated_at_lt]="
515
+ + updated_at_lt.astimezone(timezone.utc)
516
+ .replace(tzinfo=None)
517
+ .isoformat(timespec="milliseconds")
518
+ + "Z"
519
+ )
520
+ )
521
+
522
+ # Table definition
523
+ tabledef: TableDef = [
524
+ {"header": Column("Id", no_wrap=True), "column": "id"},
525
+ {"header": "Name", "column": "name"},
526
+ {"header": "Category", "column": "category"},
527
+ {"header": "Version", "column": "version"},
528
+ {"header": "Created", "column": "created_at"},
529
+ {"header": "Updated", "column": "updated_at"},
530
+ ]
531
+
532
+ # List related resources
533
+ await list_relation_resources(ctx, parent_model, "services", tabledef, modifier)
534
+
535
+
536
+ @app.async_command(name="list-products", help="List products for a service bundle")
537
+ async def list_products(
538
+ ctx: typer.Context,
539
+ service_bundle_id: Annotated[UUID, typer.Argument(help="Parent resource ID")],
540
+ name: Annotated[Optional[str], typer.Option(help="Product name")] = None,
541
+ product_type: Annotated[
542
+ Optional[str],
543
+ typer.Option(help="Type of product (option only available for child products)"),
544
+ ] = None,
545
+ price: Annotated[Optional[str], typer.Option(help="Product price")] = None,
546
+ default_bill_sequence: Annotated[
547
+ Optional[str],
548
+ typer.Option(help="Default billing sequence (only for management products)"),
549
+ ] = None,
550
+ invoice_description: Annotated[
551
+ Optional[str], typer.Option(help="Default description for the invoice item")
552
+ ] = None,
553
+ options: Annotated[
554
+ Optional[str], typer.Option(help="Product options in JSON format")
555
+ ] = None,
556
+ account: Annotated[
557
+ Optional[UUID],
558
+ typer.Option(
559
+ help="The account that owns this product (null for a global product)"
560
+ ),
561
+ ] = None,
562
+ parent: Annotated[
563
+ Optional[UUID],
564
+ typer.Option(help="The parent product (if this is a child product)"),
565
+ ] = None,
566
+ product_location: Annotated[
567
+ Optional[UUID],
568
+ typer.Option(help="The location where this product is delivered"),
569
+ ] = None,
570
+ publish_start: Annotated[
571
+ Optional[datetime],
572
+ typer.Option(help="Start of the publication window (exact match)"),
573
+ ] = None,
574
+ publish_start_gte: Annotated[
575
+ Optional[datetime],
576
+ typer.Option(help="Start of the publication window (greater than or equal)"),
577
+ ] = None,
578
+ publish_start_lte: Annotated[
579
+ Optional[datetime],
580
+ typer.Option(help="Start of the publication window (less than or equal)"),
581
+ ] = None,
582
+ publish_start_gt: Annotated[
583
+ Optional[datetime],
584
+ typer.Option(help="Start of the publication window (greater than)"),
585
+ ] = None,
586
+ publish_start_lt: Annotated[
587
+ Optional[datetime],
588
+ typer.Option(help="Start of the publication window (less than)"),
589
+ ] = None,
590
+ publish_end: Annotated[
591
+ Optional[datetime],
592
+ typer.Option(help="End of the publication window (exact match)"),
593
+ ] = None,
594
+ publish_end_gte: Annotated[
595
+ Optional[datetime],
596
+ typer.Option(help="End of the publication window (greater than or equal)"),
597
+ ] = None,
598
+ publish_end_lte: Annotated[
599
+ Optional[datetime],
600
+ typer.Option(help="End of the publication window (less than or equal)"),
601
+ ] = None,
602
+ publish_end_gt: Annotated[
603
+ Optional[datetime],
604
+ typer.Option(help="End of the publication window (greater than)"),
605
+ ] = None,
606
+ publish_end_lt: Annotated[
607
+ Optional[datetime],
608
+ typer.Option(help="End of the publication window (less than)"),
609
+ ] = None,
610
+ created_at: Annotated[
611
+ Optional[datetime], typer.Option(help=" (exact match)")
612
+ ] = None,
613
+ created_at_gte: Annotated[
614
+ Optional[datetime], typer.Option(help=" (greater than or equal)")
615
+ ] = None,
616
+ created_at_lte: Annotated[
617
+ Optional[datetime], typer.Option(help=" (less than or equal)")
618
+ ] = None,
619
+ created_at_gt: Annotated[
620
+ Optional[datetime], typer.Option(help=" (greater than)")
621
+ ] = None,
622
+ created_at_lt: Annotated[
623
+ Optional[datetime], typer.Option(help=" (less than)")
624
+ ] = None,
625
+ updated_at: Annotated[
626
+ Optional[datetime], typer.Option(help=" (exact match)")
627
+ ] = None,
628
+ updated_at_gte: Annotated[
629
+ Optional[datetime], typer.Option(help=" (greater than or equal)")
630
+ ] = None,
631
+ updated_at_lte: Annotated[
632
+ Optional[datetime], typer.Option(help=" (less than or equal)")
633
+ ] = None,
634
+ updated_at_gt: Annotated[
635
+ Optional[datetime], typer.Option(help=" (greater than)")
636
+ ] = None,
637
+ updated_at_lt: Annotated[
638
+ Optional[datetime], typer.Option(help=" (less than)")
639
+ ] = None,
640
+ ):
641
+ """List products for a service bundle"""
642
+ async with Connection() as conn:
643
+ # Fetch parent resource
644
+ parent_ctrl = ServiceBundle(conn, api_schema)
645
+ parent_model = await parent_ctrl.fetch(service_bundle_id)
646
+
647
+ # Build modifier for filtering and includes
648
+ modifier = []
649
+
650
+ if name is not None:
651
+ modifier.append(Filter(name=name))
652
+
653
+ if product_type is not None:
654
+ modifier.append(
655
+ Filter(query_str="filter[product_type]=" + str(product_type))
656
+ )
657
+
658
+ if price is not None:
659
+ modifier.append(Filter(price=price))
660
+
661
+ if default_bill_sequence is not None:
662
+ modifier.append(
663
+ Filter(
664
+ query_str="filter[default_bill_sequence]="
665
+ + str(default_bill_sequence)
666
+ )
667
+ )
668
+
669
+ if invoice_description is not None:
670
+ modifier.append(
671
+ Filter(
672
+ query_str="filter[invoice_description]=" + str(invoice_description)
673
+ )
674
+ )
675
+
676
+ if options is not None:
677
+ modifier.append(Filter(options=options))
678
+
679
+ if account is not None:
680
+ modifier.append(Filter(account=str(account)))
681
+
682
+ if parent is not None:
683
+ modifier.append(Filter(parent=str(parent)))
684
+
685
+ if product_location is not None:
686
+ modifier.append(
687
+ Filter(query_str="filter[product-location]=" + str(product_location))
688
+ )
689
+
690
+ if publish_start is not None:
691
+ modifier.append(
692
+ Filter(
693
+ query_str="filter[publish_start]="
694
+ + publish_start.astimezone(timezone.utc)
695
+ .replace(tzinfo=None)
696
+ .isoformat(timespec="milliseconds")
697
+ + "Z"
698
+ )
699
+ )
700
+ if publish_start_gte is not None:
701
+ modifier.append(
702
+ Filter(
703
+ query_str="filter[publish_start_gte]="
704
+ + publish_start_gte.astimezone(timezone.utc)
705
+ .replace(tzinfo=None)
706
+ .isoformat(timespec="milliseconds")
707
+ + "Z"
708
+ )
709
+ )
710
+ if publish_start_lte is not None:
711
+ modifier.append(
712
+ Filter(
713
+ query_str="filter[publish_start_lte]="
714
+ + publish_start_lte.astimezone(timezone.utc)
715
+ .replace(tzinfo=None)
716
+ .isoformat(timespec="milliseconds")
717
+ + "Z"
718
+ )
719
+ )
720
+ if publish_start_gt is not None:
721
+ modifier.append(
722
+ Filter(
723
+ query_str="filter[publish_start_gt]="
724
+ + publish_start_gt.astimezone(timezone.utc)
725
+ .replace(tzinfo=None)
726
+ .isoformat(timespec="milliseconds")
727
+ + "Z"
728
+ )
729
+ )
730
+ if publish_start_lt is not None:
731
+ modifier.append(
732
+ Filter(
733
+ query_str="filter[publish_start_lt]="
734
+ + publish_start_lt.astimezone(timezone.utc)
735
+ .replace(tzinfo=None)
736
+ .isoformat(timespec="milliseconds")
737
+ + "Z"
738
+ )
739
+ )
740
+
741
+ if publish_end is not None:
742
+ modifier.append(
743
+ Filter(
744
+ query_str="filter[publish_end]="
745
+ + publish_end.astimezone(timezone.utc)
746
+ .replace(tzinfo=None)
747
+ .isoformat(timespec="milliseconds")
748
+ + "Z"
749
+ )
750
+ )
751
+ if publish_end_gte is not None:
752
+ modifier.append(
753
+ Filter(
754
+ query_str="filter[publish_end_gte]="
755
+ + publish_end_gte.astimezone(timezone.utc)
756
+ .replace(tzinfo=None)
757
+ .isoformat(timespec="milliseconds")
758
+ + "Z"
759
+ )
760
+ )
761
+ if publish_end_lte is not None:
762
+ modifier.append(
763
+ Filter(
764
+ query_str="filter[publish_end_lte]="
765
+ + publish_end_lte.astimezone(timezone.utc)
766
+ .replace(tzinfo=None)
767
+ .isoformat(timespec="milliseconds")
768
+ + "Z"
769
+ )
770
+ )
771
+ if publish_end_gt is not None:
772
+ modifier.append(
773
+ Filter(
774
+ query_str="filter[publish_end_gt]="
775
+ + publish_end_gt.astimezone(timezone.utc)
776
+ .replace(tzinfo=None)
777
+ .isoformat(timespec="milliseconds")
778
+ + "Z"
779
+ )
780
+ )
781
+ if publish_end_lt is not None:
782
+ modifier.append(
783
+ Filter(
784
+ query_str="filter[publish_end_lt]="
785
+ + publish_end_lt.astimezone(timezone.utc)
786
+ .replace(tzinfo=None)
787
+ .isoformat(timespec="milliseconds")
788
+ + "Z"
789
+ )
790
+ )
791
+
792
+ if created_at is not None:
793
+ modifier.append(
794
+ Filter(
795
+ query_str="filter[created_at]="
796
+ + created_at.astimezone(timezone.utc)
797
+ .replace(tzinfo=None)
798
+ .isoformat(timespec="milliseconds")
799
+ + "Z"
800
+ )
801
+ )
802
+ if created_at_gte is not None:
803
+ modifier.append(
804
+ Filter(
805
+ query_str="filter[created_at_gte]="
806
+ + created_at_gte.astimezone(timezone.utc)
807
+ .replace(tzinfo=None)
808
+ .isoformat(timespec="milliseconds")
809
+ + "Z"
810
+ )
811
+ )
812
+ if created_at_lte is not None:
813
+ modifier.append(
814
+ Filter(
815
+ query_str="filter[created_at_lte]="
816
+ + created_at_lte.astimezone(timezone.utc)
817
+ .replace(tzinfo=None)
818
+ .isoformat(timespec="milliseconds")
819
+ + "Z"
820
+ )
821
+ )
822
+ if created_at_gt is not None:
823
+ modifier.append(
824
+ Filter(
825
+ query_str="filter[created_at_gt]="
826
+ + created_at_gt.astimezone(timezone.utc)
827
+ .replace(tzinfo=None)
828
+ .isoformat(timespec="milliseconds")
829
+ + "Z"
830
+ )
831
+ )
832
+ if created_at_lt is not None:
833
+ modifier.append(
834
+ Filter(
835
+ query_str="filter[created_at_lt]="
836
+ + created_at_lt.astimezone(timezone.utc)
837
+ .replace(tzinfo=None)
838
+ .isoformat(timespec="milliseconds")
839
+ + "Z"
840
+ )
841
+ )
842
+
843
+ if updated_at is not None:
844
+ modifier.append(
845
+ Filter(
846
+ query_str="filter[updated_at]="
847
+ + updated_at.astimezone(timezone.utc)
848
+ .replace(tzinfo=None)
849
+ .isoformat(timespec="milliseconds")
850
+ + "Z"
851
+ )
852
+ )
853
+ if updated_at_gte is not None:
854
+ modifier.append(
855
+ Filter(
856
+ query_str="filter[updated_at_gte]="
857
+ + updated_at_gte.astimezone(timezone.utc)
858
+ .replace(tzinfo=None)
859
+ .isoformat(timespec="milliseconds")
860
+ + "Z"
861
+ )
862
+ )
863
+ if updated_at_lte is not None:
864
+ modifier.append(
865
+ Filter(
866
+ query_str="filter[updated_at_lte]="
867
+ + updated_at_lte.astimezone(timezone.utc)
868
+ .replace(tzinfo=None)
869
+ .isoformat(timespec="milliseconds")
870
+ + "Z"
871
+ )
872
+ )
873
+ if updated_at_gt is not None:
874
+ modifier.append(
875
+ Filter(
876
+ query_str="filter[updated_at_gt]="
877
+ + updated_at_gt.astimezone(timezone.utc)
878
+ .replace(tzinfo=None)
879
+ .isoformat(timespec="milliseconds")
880
+ + "Z"
881
+ )
882
+ )
883
+ if updated_at_lt is not None:
884
+ modifier.append(
885
+ Filter(
886
+ query_str="filter[updated_at_lt]="
887
+ + updated_at_lt.astimezone(timezone.utc)
888
+ .replace(tzinfo=None)
889
+ .isoformat(timespec="milliseconds")
890
+ + "Z"
891
+ )
892
+ )
893
+
894
+ # Table definition
895
+ tabledef: TableDef = [
896
+ {"header": Column("Id", no_wrap=True), "column": "id"},
897
+ {"header": "Name", "column": "name"},
898
+ {"header": "Product Type", "column": "product_type"},
899
+ {"header": "Price", "column": "price"},
900
+ {"header": "Created", "column": "created_at"},
901
+ {"header": "Updated", "column": "updated_at"},
902
+ ]
903
+
904
+ # List related resources
905
+ await list_relation_resources(ctx, parent_model, "products", tabledef, modifier)
@@ -15,6 +15,7 @@ import json
15
15
  from cacholong_sdk import BaseController, BaseModel
16
16
  from cacholong_sdk import Modifier, Sort, SparseField, DocumentError
17
17
  from cacholong_sdk.exception import ValidationException
18
+ from jsonapi_client.relationships import SingleRelationship
18
19
  from uuid import UUID
19
20
 
20
21
  ErrorConsole = Console(stderr=True)
@@ -151,6 +152,20 @@ def _flatten(exc: BaseException) -> List[BaseException]:
151
152
  return [exc]
152
153
 
153
154
 
155
+ def _is_empty_to_one(relationship: Any) -> bool:
156
+ """True for a to-one relationship without linkage.
157
+
158
+ The API may omit a relationship entirely (e.g. ``account`` on a global
159
+ service bundle). The client then builds it from the schema as an
160
+ identifier without type and id; fetching that would request
161
+ ``<api_url>/None/None``.
162
+ """
163
+ return (
164
+ isinstance(relationship, SingleRelationship)
165
+ and relationship.as_json_resource_identifiers is None
166
+ )
167
+
168
+
154
169
  async def _perform_list_resources(
155
170
  controller: BaseController,
156
171
  modifier: Modifier,
@@ -164,7 +179,8 @@ async def _perform_list_resources(
164
179
  ]
165
180
  async for resource in controller.fetch_all(modifier):
166
181
  for relation in relations:
167
- await resource[relation].fetch()
182
+ if not _is_empty_to_one(resource[relation]):
183
+ await resource[relation].fetch()
168
184
  callback(resource)
169
185
 
170
186
 
@@ -179,8 +195,9 @@ async def _perform_list_relation_resources(
179
195
  def _handle_column(row: BaseModel, column_definition: Dict[str, Any]) -> str:
180
196
  if "nested_column" in column_definition:
181
197
  column_key = column_definition.get("column_kebab", column_definition["column"])
182
- if row[column_key].resource != None:
183
- return str(row[column_key].resource[column_definition["nested_column"]])
198
+ relationship = row[column_key]
199
+ if not _is_empty_to_one(relationship) and relationship.resource != None:
200
+ return str(relationship.resource[column_definition["nested_column"]])
184
201
  return ""
185
202
  else:
186
203
  return str(row[column_definition["column"]])
@@ -19,6 +19,8 @@ from .service_mails import ServiceMailModel, ServiceMail
19
19
  from .mail_forwards import MailForwardModel, MailForward
20
20
  from .products import ProductModel, Product
21
21
  from .purchases import PurchaseModel, Purchase
22
+ from .service_bundles import ServiceBundleModel, ServiceBundle
23
+ from .services import ServiceModel, Service
22
24
 
23
25
  # Import directly from jsonapi
24
26
  from jsonapi_client import Modifier, Filter, Inclusion, Sort, SparseField
@@ -302,7 +302,7 @@ api_schema = {
302
302
  },
303
303
  "dns-zones:update": {
304
304
  "properties": {
305
- "domain": {"type": ["string"]},
305
+ "domain": {"type": ["string", "null"]},
306
306
  "active": {"type": ["boolean", "null"]},
307
307
  "dnssec": {"type": ["boolean", "null"]},
308
308
  "account": {"relation": "to-one", "resource": "accounts"},
@@ -817,4 +817,65 @@ api_schema = {
817
817
  "product": {"relation": "to-one", "resource": "products"},
818
818
  }
819
819
  },
820
+ "service-bundles": {
821
+ "properties": {
822
+ "name": {"type": ["string"]},
823
+ "description": {"type": ["string", "null"]},
824
+ "created_at": {"type": ["string", "null"]},
825
+ "updated_at": {"type": ["string", "null"]},
826
+ "account": {"relation": "to-one", "resource": "accounts"},
827
+ "services": {"relation": "to-many", "resource": "services"},
828
+ "products": {"relation": "to-many", "resource": "products"},
829
+ }
830
+ },
831
+ "service-bundles:base": {
832
+ "properties": {
833
+ "name": {"type": ["string"]},
834
+ "description": {"type": ["string", "null"]},
835
+ }
836
+ },
837
+ "service-bundles:show": {
838
+ "properties": {
839
+ "name": {"type": ["string"]},
840
+ "description": {"type": ["string", "null"]},
841
+ "created_at": {"type": ["string", "null"]},
842
+ "updated_at": {"type": ["string", "null"]},
843
+ "account": {"relation": "to-one", "resource": "accounts"},
844
+ "services": {"relation": "to-many", "resource": "services"},
845
+ "products": {"relation": "to-many", "resource": "products"},
846
+ }
847
+ },
848
+ "services": {
849
+ "properties": {
850
+ "category": {"type": ["string"]},
851
+ "name": {"type": ["string"]},
852
+ "description": {"type": ["string", "null"]},
853
+ "version": {"type": ["string"]},
854
+ "deprecated_at": {"type": ["string", "null"]},
855
+ "created_at": {"type": ["string", "null"]},
856
+ "updated_at": {"type": ["string", "null"]},
857
+ "service-bundles": {"relation": "to-many", "resource": "service-bundles"},
858
+ }
859
+ },
860
+ "services:base": {
861
+ "properties": {
862
+ "category": {"type": ["string"]},
863
+ "name": {"type": ["string"]},
864
+ "description": {"type": ["string", "null"]},
865
+ "version": {"type": ["string"]},
866
+ "deprecated_at": {"type": ["string", "null"]},
867
+ }
868
+ },
869
+ "services:show": {
870
+ "properties": {
871
+ "category": {"type": ["string"]},
872
+ "name": {"type": ["string"]},
873
+ "description": {"type": ["string", "null"]},
874
+ "version": {"type": ["string"]},
875
+ "deprecated_at": {"type": ["string", "null"]},
876
+ "created_at": {"type": ["string", "null"]},
877
+ "updated_at": {"type": ["string", "null"]},
878
+ "service-bundles": {"relation": "to-many", "resource": "service-bundles"},
879
+ }
880
+ },
820
881
  }
@@ -0,0 +1,14 @@
1
+ from typing import Optional, Dict
2
+ from .common import BaseController, BaseModel
3
+
4
+
5
+ class ServiceBundleModel(BaseModel):
6
+ pass
7
+
8
+
9
+ class ServiceBundle(BaseController[ServiceBundleModel]):
10
+ _class = ServiceBundleModel
11
+
12
+ def __init__(self, connection, api_schema: Optional[dict] = None):
13
+ self._resource = "service-bundles"
14
+ super().__init__(connection, api_schema)
@@ -0,0 +1,14 @@
1
+ from typing import Optional, Dict
2
+ from .common import BaseController, BaseModel
3
+
4
+
5
+ class ServiceModel(BaseModel):
6
+ pass
7
+
8
+
9
+ class Service(BaseController[ServiceModel]):
10
+ _class = ServiceModel
11
+
12
+ def __init__(self, connection, api_schema: Optional[dict] = None):
13
+ self._resource = "services"
14
+ super().__init__(connection, api_schema)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cacholong-cloud-cli"
3
- version = "1.13.2"
3
+ version = "1.14.1"
4
4
  description = "Cacholong CLI, manage cloud environments at Cacholong!"
5
5
  authors = ["Matthijs Möhlmann <matthijs@cacholong.nl>"]
6
6
  readme = "README.md"