vantage-python 0.3.2__tar.gz → 0.3.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vantage-python
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Python SDK for the Vantage API
5
5
  Project-URL: Homepage, https://github.com/vantage-sh/vantage-python
6
6
  Project-URL: Repository, https://github.com/vantage-sh/vantage-python
@@ -31,6 +31,12 @@ RESPONSE_HANDLERS: list[tuple[str, str, str]] = [
31
31
  ),
32
32
  ]
33
33
 
34
+ # Endpoints that return bool based on HTTP status: 404 -> False, 2xx -> True, else raise.
35
+ # Each entry is (METHOD, openapi_path_template).
36
+ BOOLEAN_STATUS_ROUTES: list[tuple[str, str]] = [
37
+ ("GET", "/virtual_tag_configs/async/{request_id}"),
38
+ ]
39
+
34
40
 
35
41
  @dataclass
36
42
  class Parameter:
@@ -61,6 +67,7 @@ class Endpoint:
61
67
  is_multipart: bool = False
62
68
  response_handler: str | None = None # internal client method to call, if not the default
63
69
  response_handler_return_type: str | None = None
70
+ boolean_status: bool = False # 404->False, 2xx->True, else raise VantageAPIError
64
71
 
65
72
 
66
73
  @dataclass
@@ -196,10 +203,16 @@ def preprocess_inline_models(schemas: dict[str, Any]) -> None:
196
203
  existing_names.add(model_name)
197
204
 
198
205
 
199
- def openapi_type_to_python(schema: dict[str, Any], schemas: dict[str, Any]) -> str:
206
+ def openapi_type_to_python(
207
+ schema: dict[str, Any],
208
+ schemas: dict[str, Any],
209
+ name_map: dict[str, str] | None = None,
210
+ ) -> str:
200
211
  """Convert OpenAPI type to Python type hint."""
201
212
  if "$ref" in schema:
202
213
  ref_name = schema["$ref"].split("/")[-1]
214
+ if name_map and ref_name in name_map:
215
+ return name_map[ref_name]
203
216
  return to_pascal_case(ref_name)
204
217
 
205
218
  schema_type = schema.get("type", "any")
@@ -216,12 +229,12 @@ def openapi_type_to_python(schema: dict[str, Any], schemas: dict[str, Any]) -> s
216
229
  return "bool"
217
230
  elif schema_type == "array":
218
231
  items = schema.get("items", {})
219
- item_type = openapi_type_to_python(items, schemas)
232
+ item_type = openapi_type_to_python(items, schemas, name_map)
220
233
  return f"List[{item_type}]"
221
234
  elif schema_type == "object":
222
235
  additional = schema.get("additionalProperties")
223
236
  if additional:
224
- value_type = openapi_type_to_python(additional, schemas)
237
+ value_type = openapi_type_to_python(additional, schemas, name_map)
225
238
  return f"Dict[str, {value_type}]"
226
239
  # Check if inline properties match an existing named schema
227
240
  inline_props = schema.get("properties")
@@ -230,6 +243,8 @@ def openapi_type_to_python(schema: dict[str, Any], schemas: dict[str, Any]) -> s
230
243
  for schema_name, schema_def in schemas.items():
231
244
  defined_keys = sorted(schema_def.get("properties", {}).keys())
232
245
  if defined_keys and inline_keys == defined_keys:
246
+ if name_map and schema_name in name_map:
247
+ return name_map[schema_name]
233
248
  return to_pascal_case(schema_name)
234
249
  return "Dict[str, Any]"
235
250
  else:
@@ -237,7 +252,9 @@ def openapi_type_to_python(schema: dict[str, Any], schemas: dict[str, Any]) -> s
237
252
 
238
253
 
239
254
  def extract_request_body_type(
240
- request_body: dict[str, Any] | None, schemas: dict[str, Any]
255
+ request_body: dict[str, Any] | None,
256
+ schemas: dict[str, Any],
257
+ name_map: dict[str, str] | None = None,
241
258
  ) -> tuple[str | None, bool]:
242
259
  """Extract request body type and whether it's multipart."""
243
260
  if not request_body:
@@ -252,13 +269,15 @@ def extract_request_body_type(
252
269
  # Check for JSON
253
270
  if "application/json" in content:
254
271
  schema = content["application/json"].get("schema", {})
255
- return openapi_type_to_python(schema, schemas), False
272
+ return openapi_type_to_python(schema, schemas, name_map), False
256
273
 
257
274
  return None, False
258
275
 
259
276
 
260
277
  def extract_response_type(
261
- responses: dict[str, Any], schemas: dict[str, Any]
278
+ responses: dict[str, Any],
279
+ schemas: dict[str, Any],
280
+ name_map: dict[str, str] | None = None,
262
281
  ) -> str | None:
263
282
  """Extract successful response type."""
264
283
  for code in ["200", "201", "202", "203"]:
@@ -268,15 +287,59 @@ def extract_response_type(
268
287
  content = response.get("content", {})
269
288
  if "application/json" in content:
270
289
  schema = content["application/json"].get("schema", {})
271
- return openapi_type_to_python(schema, schemas)
290
+ return openapi_type_to_python(schema, schemas, name_map)
272
291
  return None
273
292
 
274
293
 
294
+ def find_request_body_schemas(schema: dict[str, Any]) -> set[str]:
295
+ """Return the set of schema names referenced as request bodies in any endpoint."""
296
+ result = set()
297
+ paths = schema.get("paths", {})
298
+ for path_item in paths.values():
299
+ for method, spec in path_item.items():
300
+ if method in ("parameters", "servers", "summary", "description"):
301
+ continue
302
+ request_body = spec.get("requestBody", {})
303
+ content = request_body.get("content", {})
304
+ for media_type in content.values():
305
+ ref_schema = media_type.get("schema", {})
306
+ if "$ref" in ref_schema:
307
+ name = ref_schema["$ref"].split("/")[-1]
308
+ result.add(name)
309
+ return result
310
+
311
+
312
+ def build_class_name_map(schemas: dict[str, Any], request_body_schemas: set[str]) -> dict[str, str]:
313
+ """Build a mapping from raw schema names to Python class names, resolving conflicts.
314
+
315
+ If two schema names map to the same PascalCase name, the one used as a
316
+ request body is suffixed with 'Request'.
317
+ """
318
+ initial = {name: to_pascal_case(name) for name in schemas}
319
+
320
+ by_class_name: dict[str, list[str]] = {}
321
+ for raw_name, class_name in initial.items():
322
+ by_class_name.setdefault(class_name, []).append(raw_name)
323
+
324
+ result: dict[str, str] = {}
325
+ for class_name, raw_names in by_class_name.items():
326
+ if len(raw_names) == 1:
327
+ result[raw_names[0]] = class_name
328
+ else:
329
+ for raw_name in raw_names:
330
+ if raw_name in request_body_schemas:
331
+ result[raw_name] = class_name + "Request"
332
+ else:
333
+ result[raw_name] = class_name
334
+ return result
335
+
336
+
275
337
  def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
276
338
  """Parse all endpoints from OpenAPI schema."""
277
339
  endpoints = []
278
340
  paths = schema.get("paths", {})
279
341
  schemas = schema.get("components", {}).get("schemas", {})
342
+ name_map = build_class_name_map(schemas, find_request_body_schemas(schema))
280
343
 
281
344
  for path, methods in paths.items():
282
345
  for method, spec in methods.items():
@@ -288,7 +351,7 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
288
351
  parameters = []
289
352
  for param in spec.get("parameters", []):
290
353
  param_schema = param.get("schema", {})
291
- param_type = openapi_type_to_python(param_schema, schemas)
354
+ param_type = openapi_type_to_python(param_schema, schemas, name_map)
292
355
  parameters.append(
293
356
  Parameter(
294
357
  name=param["name"],
@@ -301,9 +364,9 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
301
364
  )
302
365
 
303
366
  request_body = spec.get("requestBody")
304
- body_type, is_multipart = extract_request_body_type(request_body, schemas)
367
+ body_type, is_multipart = extract_request_body_type(request_body, schemas, name_map)
305
368
 
306
- response_type = extract_response_type(spec.get("responses", {}), schemas)
369
+ response_type = extract_response_type(spec.get("responses", {}), schemas, name_map)
307
370
 
308
371
  description = spec.get("description")
309
372
 
@@ -319,6 +382,10 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
319
382
  if response_handler:
320
383
  break
321
384
 
385
+ boolean_status = (method.upper(), path) in {
386
+ (m.upper(), p) for m, p in BOOLEAN_STATUS_ROUTES
387
+ }
388
+
322
389
  endpoints.append(
323
390
  Endpoint(
324
391
  path=path,
@@ -336,6 +403,7 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
336
403
  is_multipart=is_multipart,
337
404
  response_handler=response_handler,
338
405
  response_handler_return_type=response_handler_return_type,
406
+ boolean_status=boolean_status,
339
407
  )
340
408
  )
341
409
 
@@ -469,6 +537,7 @@ def _append_response_mapping(lines: list[str], return_type: str, data_var: str)
469
537
  def generate_pydantic_models(schema: dict[str, Any]) -> str:
470
538
  """Generate Pydantic models from OpenAPI schemas."""
471
539
  schemas = schema.get("components", {}).get("schemas", {})
540
+ name_map = build_class_name_map(schemas, find_request_body_schemas(schema))
472
541
  lines = [
473
542
  '"""Auto-generated Pydantic models from OpenAPI schema."""',
474
543
  "",
@@ -482,7 +551,7 @@ def generate_pydantic_models(schema: dict[str, Any]) -> str:
482
551
  ]
483
552
 
484
553
  for name, spec in schemas.items():
485
- class_name = to_pascal_case(name)
554
+ class_name = name_map.get(name, to_pascal_case(name))
486
555
  description = spec.get("description", "")
487
556
 
488
557
  lines.append(f"class {class_name}(BaseModel):")
@@ -508,7 +577,7 @@ def generate_pydantic_models(schema: dict[str, Any]) -> str:
508
577
  python_name = python_name + "_"
509
578
  needs_alias = True
510
579
 
511
- prop_type = openapi_type_to_python(prop_spec, schemas)
580
+ prop_type = openapi_type_to_python(prop_spec, schemas, name_map)
512
581
 
513
582
  # Handle nullable
514
583
  if prop_spec.get("x-nullable") or prop_spec.get("nullable"):
@@ -564,6 +633,21 @@ def _collect_handler_routes(resources: dict[str, Resource]) -> dict[str, list[tu
564
633
  return handler_routes
565
634
 
566
635
 
636
+ def _collect_boolean_status_prefixes(resources: dict[str, Resource]) -> list[tuple[str, str]]:
637
+ """Collect (method, path_prefix) pairs for boolean-status endpoints.
638
+
639
+ The prefix is derived by taking everything before the first path parameter
640
+ so it can be matched with str.startswith() at runtime.
641
+ """
642
+ result = []
643
+ for resource in resources.values():
644
+ for endpoint in resource.endpoints:
645
+ if endpoint.boolean_status:
646
+ prefix = endpoint.path.split("{")[0]
647
+ result.append((endpoint.method, prefix))
648
+ return result
649
+
650
+
567
651
  def generate_sync_client(resources: dict[str, Resource]) -> str:
568
652
  """Generate synchronous client code."""
569
653
  lines = [
@@ -652,6 +736,29 @@ def generate_sync_client(resources: dict[str, Resource]) -> str:
652
736
  " json=body,",
653
737
  " )",
654
738
  "",
739
+ ]
740
+ )
741
+
742
+ # Inject boolean-status path checks (before the generic error check)
743
+ boolean_prefixes = _collect_boolean_status_prefixes(resources)
744
+ for method, prefix in boolean_prefixes:
745
+ lines.extend([
746
+ f' if method.upper() == "{method}" and path.startswith("{prefix}"):',
747
+ " if response.status_code == 404:",
748
+ " return False",
749
+ " elif response.is_success:",
750
+ " return True",
751
+ " else:",
752
+ " raise VantageAPIError(",
753
+ " status=response.status_code,",
754
+ " status_text=response.reason_phrase,",
755
+ " body=response.text,",
756
+ " )",
757
+ "",
758
+ ])
759
+
760
+ lines.extend(
761
+ [
655
762
  " if not response.is_success:",
656
763
  " raise VantageAPIError(",
657
764
  " status=response.status_code,",
@@ -757,7 +864,9 @@ def generate_sync_method(endpoint: Endpoint, method_name: str) -> list[str]:
757
864
 
758
865
  # Method signature
759
866
  param_str = ", ".join(["self"] + params) if params else "self"
760
- if endpoint.response_handler:
867
+ if endpoint.boolean_status:
868
+ return_type = "bool"
869
+ elif endpoint.response_handler:
761
870
  return_type = endpoint.response_handler_return_type or "Any"
762
871
  else:
763
872
  return_type = endpoint.response_type or "None"
@@ -801,7 +910,7 @@ def generate_sync_method(endpoint: Endpoint, method_name: str) -> list[str]:
801
910
  lines.append(" body_data = None")
802
911
 
803
912
  # Make request and coerce response payload into typed models where possible
804
- if endpoint.response_handler:
913
+ if endpoint.boolean_status or endpoint.response_handler:
805
914
  lines.append(
806
915
  f' return self._client.request("{endpoint.method}", path, params=params, body=body_data)'
807
916
  )
@@ -907,6 +1016,29 @@ def generate_async_client(resources: dict[str, Resource]) -> str:
907
1016
  " json=body,",
908
1017
  " )",
909
1018
  "",
1019
+ ]
1020
+ )
1021
+
1022
+ # Inject boolean-status path checks (before the generic error check)
1023
+ boolean_prefixes = _collect_boolean_status_prefixes(resources)
1024
+ for method, prefix in boolean_prefixes:
1025
+ lines.extend([
1026
+ f' if method.upper() == "{method}" and path.startswith("{prefix}"):',
1027
+ " if response.status_code == 404:",
1028
+ " return False",
1029
+ " elif response.is_success:",
1030
+ " return True",
1031
+ " else:",
1032
+ " raise VantageAPIError(",
1033
+ " status=response.status_code,",
1034
+ " status_text=response.reason_phrase,",
1035
+ " body=response.text,",
1036
+ " )",
1037
+ "",
1038
+ ])
1039
+
1040
+ lines.extend(
1041
+ [
910
1042
  " if not response.is_success:",
911
1043
  " raise VantageAPIError(",
912
1044
  " status=response.status_code,",
@@ -1012,7 +1144,9 @@ def generate_async_method(endpoint: Endpoint, method_name: str) -> list[str]:
1012
1144
 
1013
1145
  # Method signature
1014
1146
  param_str = ", ".join(["self"] + params) if params else "self"
1015
- if endpoint.response_handler:
1147
+ if endpoint.boolean_status:
1148
+ return_type = "bool"
1149
+ elif endpoint.response_handler:
1016
1150
  return_type = endpoint.response_handler_return_type or "Any"
1017
1151
  else:
1018
1152
  return_type = endpoint.response_type or "None"
@@ -1056,7 +1190,7 @@ def generate_async_method(endpoint: Endpoint, method_name: str) -> list[str]:
1056
1190
  lines.append(" body_data = None")
1057
1191
 
1058
1192
  # Make request and coerce response payload into typed models where possible
1059
- if endpoint.response_handler:
1193
+ if endpoint.boolean_status or endpoint.response_handler:
1060
1194
  lines.append(
1061
1195
  f' return await self._client.request("{endpoint.method}", path, params=params, body=body_data)'
1062
1196
  )
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vantage-python"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Python SDK for the Vantage API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -116,6 +116,18 @@ class AsyncClient:
116
116
  json=body,
117
117
  )
118
118
 
119
+ if method.upper() == "GET" and path.startswith("/virtual_tag_configs/async/"):
120
+ if response.status_code == 404:
121
+ return False
122
+ elif response.is_success:
123
+ return True
124
+ else:
125
+ raise VantageAPIError(
126
+ status=response.status_code,
127
+ status_text=response.reason_phrase,
128
+ body=response.text,
129
+ )
130
+
119
131
  if not response.is_success:
120
132
  raise VantageAPIError(
121
133
  status=response.status_code,
@@ -1674,7 +1686,7 @@ class InvoicesAsyncApi:
1674
1686
  return Invoice.model_validate(data)
1675
1687
  return data
1676
1688
 
1677
- async def download(self, invoice_token: str, body: DownloadInvoice) -> None:
1689
+ async def download(self, invoice_token: str, body: DownloadInvoiceRequest) -> DownloadInvoice:
1678
1690
  """
1679
1691
  Get invoice file
1680
1692
 
@@ -1683,7 +1695,10 @@ class InvoicesAsyncApi:
1683
1695
  path = f"/invoices/{quote(str(invoice_token), safe='')}/download"
1684
1696
  params = None
1685
1697
  body_data = body.model_dump(by_alias=True, exclude_none=True) if hasattr(body, 'model_dump') else body
1686
- await self._client.request("POST", path, params=params, body=body_data)
1698
+ data = await self._client.request("POST", path, params=params, body=body_data)
1699
+ if isinstance(data, dict):
1700
+ return DownloadInvoice.model_validate(data)
1701
+ return data
1687
1702
 
1688
1703
  async def send(self, invoice_token: str) -> SendInvoice:
1689
1704
  """
@@ -3042,7 +3057,7 @@ class VirtualTagConfigsAsyncApi:
3042
3057
  return VirtualTagConfigStatus.model_validate(data)
3043
3058
  return data
3044
3059
 
3045
- async def update_async(self, token: str, body: UpdateAsyncVirtualTagConfig) -> None:
3060
+ async def update_async(self, token: str, body: UpdateAsyncVirtualTagConfig) -> AsyncVirtualTagConfigUpdate:
3046
3061
  """
3047
3062
  Update virtual tag config asynchronously
3048
3063
 
@@ -3051,9 +3066,12 @@ class VirtualTagConfigsAsyncApi:
3051
3066
  path = f"/virtual_tag_configs/{quote(str(token), safe='')}/async"
3052
3067
  params = None
3053
3068
  body_data = body.model_dump(by_alias=True, exclude_none=True) if hasattr(body, 'model_dump') else body
3054
- await self._client.request("PUT", path, params=params, body=body_data)
3069
+ data = await self._client.request("PUT", path, params=params, body=body_data)
3070
+ if isinstance(data, dict):
3071
+ return AsyncVirtualTagConfigUpdate.model_validate(data)
3072
+ return data
3055
3073
 
3056
- async def get_async_virtual_tag_config_status(self, request_id: str) -> None:
3074
+ async def get_async_virtual_tag_config_status(self, request_id: str) -> bool:
3057
3075
  """
3058
3076
  Get async virtual tag config update status
3059
3077
 
@@ -3062,7 +3080,7 @@ class VirtualTagConfigsAsyncApi:
3062
3080
  path = f"/virtual_tag_configs/async/{quote(str(request_id), safe='')}"
3063
3081
  params = None
3064
3082
  body_data = None
3065
- await self._client.request("GET", path, params=params, body=body_data)
3083
+ return await self._client.request("GET", path, params=params, body=body_data)
3066
3084
 
3067
3085
 
3068
3086
  class WorkspacesAsyncApi:
@@ -116,6 +116,18 @@ class SyncClient:
116
116
  json=body,
117
117
  )
118
118
 
119
+ if method.upper() == "GET" and path.startswith("/virtual_tag_configs/async/"):
120
+ if response.status_code == 404:
121
+ return False
122
+ elif response.is_success:
123
+ return True
124
+ else:
125
+ raise VantageAPIError(
126
+ status=response.status_code,
127
+ status_text=response.reason_phrase,
128
+ body=response.text,
129
+ )
130
+
119
131
  if not response.is_success:
120
132
  raise VantageAPIError(
121
133
  status=response.status_code,
@@ -1674,7 +1686,7 @@ class InvoicesApi:
1674
1686
  return Invoice.model_validate(data)
1675
1687
  return data
1676
1688
 
1677
- def download(self, invoice_token: str, body: DownloadInvoice) -> None:
1689
+ def download(self, invoice_token: str, body: DownloadInvoiceRequest) -> DownloadInvoice:
1678
1690
  """
1679
1691
  Get invoice file
1680
1692
 
@@ -1683,7 +1695,10 @@ class InvoicesApi:
1683
1695
  path = f"/invoices/{quote(str(invoice_token), safe='')}/download"
1684
1696
  params = None
1685
1697
  body_data = body.model_dump(by_alias=True, exclude_none=True) if hasattr(body, 'model_dump') else body
1686
- self._client.request("POST", path, params=params, body=body_data)
1698
+ data = self._client.request("POST", path, params=params, body=body_data)
1699
+ if isinstance(data, dict):
1700
+ return DownloadInvoice.model_validate(data)
1701
+ return data
1687
1702
 
1688
1703
  def send(self, invoice_token: str) -> SendInvoice:
1689
1704
  """
@@ -3042,7 +3057,7 @@ class VirtualTagConfigsApi:
3042
3057
  return VirtualTagConfigStatus.model_validate(data)
3043
3058
  return data
3044
3059
 
3045
- def update_async(self, token: str, body: UpdateAsyncVirtualTagConfig) -> None:
3060
+ def update_async(self, token: str, body: UpdateAsyncVirtualTagConfig) -> AsyncVirtualTagConfigUpdate:
3046
3061
  """
3047
3062
  Update virtual tag config asynchronously
3048
3063
 
@@ -3051,9 +3066,12 @@ class VirtualTagConfigsApi:
3051
3066
  path = f"/virtual_tag_configs/{quote(str(token), safe='')}/async"
3052
3067
  params = None
3053
3068
  body_data = body.model_dump(by_alias=True, exclude_none=True) if hasattr(body, 'model_dump') else body
3054
- self._client.request("PUT", path, params=params, body=body_data)
3069
+ data = self._client.request("PUT", path, params=params, body=body_data)
3070
+ if isinstance(data, dict):
3071
+ return AsyncVirtualTagConfigUpdate.model_validate(data)
3072
+ return data
3055
3073
 
3056
- def get_async_virtual_tag_config_status(self, request_id: str) -> None:
3074
+ def get_async_virtual_tag_config_status(self, request_id: str) -> bool:
3057
3075
  """
3058
3076
  Get async virtual tag config update status
3059
3077
 
@@ -3062,7 +3080,7 @@ class VirtualTagConfigsApi:
3062
3080
  path = f"/virtual_tag_configs/async/{quote(str(request_id), safe='')}"
3063
3081
  params = None
3064
3082
  body_data = None
3065
- self._client.request("GET", path, params=params, body=body_data)
3083
+ return self._client.request("GET", path, params=params, body=body_data)
3066
3084
 
3067
3085
 
3068
3086
  class WorkspacesApi:
@@ -1021,12 +1021,18 @@ class CreateInvoice(BaseModel):
1021
1021
  account_token: str = Field(description="Token of the managed account to invoice")
1022
1022
 
1023
1023
 
1024
- class DownloadInvoice(BaseModel):
1024
+ class DownloadInvoiceRequest(BaseModel):
1025
1025
  """Download invoice file (PDF or CSV)."""
1026
1026
 
1027
1027
  file_type: str = Field(description="Type of file to download (pdf or csv)")
1028
1028
 
1029
1029
 
1030
+ class DownloadInvoice(BaseModel):
1031
+ """DownloadInvoice model"""
1032
+
1033
+ download_url: str = Field(description="The URL to download the invoice file.")
1034
+
1035
+
1030
1036
  class SendInvoice(BaseModel):
1031
1037
  """SendInvoice model"""
1032
1038
 
@@ -1120,6 +1126,8 @@ class ManagedAccount(BaseModel):
1120
1126
  billing_rule_tokens: List[str] = Field(description="The tokens for the Billing Rules assigned to the Managed Account.")
1121
1127
  email_domain: Optional[str] = Field(default=None, description="Email domain associated with this Managed Account for SSO.")
1122
1128
  msp_billing_profile_token: Optional[str] = Field(default=None, description="Token of the MSP billing profile used for this managed account (MSP invoicing accounts only)")
1129
+ payment_terms_days: Optional[int] = Field(default=None, description="Number of days until payment is due after invoice date (MSP invoicing accounts only)")
1130
+ include_managed_account_integrations: Optional[bool] = Field(default=None, description="Whether to include managed account's own integrations in invoice cost calculations (MSP invoicing accounts only)")
1123
1131
  billing_information_attributes: Optional[BillingInformation] = Field(default=None)
1124
1132
  business_information_attributes: Optional[BusinessInformation] = Field(default=None)
1125
1133
 
@@ -1143,6 +1151,8 @@ class UpdateManagedAccount(BaseModel):
1143
1151
  billing_rule_tokens: Optional[List[str]] = Field(default=None, description="Billing Rule tokens to assign to the Managed Account.")
1144
1152
  email_domain: Optional[str] = Field(default=None, description="Email domain to associate with this Managed Account for SSO.")
1145
1153
  msp_billing_profile_token: Optional[str] = Field(default=None, description="Token of the MSP billing profile to use for this managed account (MSP invoicing accounts only).")
1154
+ payment_terms_days: Optional[int] = Field(default=None, description="Number of days until payment is due after invoice date (MSP invoicing accounts only). Defaults to 10.")
1155
+ include_managed_account_integrations: Optional[bool] = Field(default=None, description="Whether to include managed account's own integrations in invoice cost calculations (MSP invoicing accounts only). Defaults to false.")
1146
1156
  billing_information_attributes: Optional[BillingInformationAttributes] = Field(default=None, description="Billing address and contact information (MSP invoicing accounts only)")
1147
1157
  business_information_attributes: Optional[BusinessInformationAttributes] = Field(default=None, description="Business information and custom fields (MSP invoicing accounts only)")
1148
1158
 
@@ -1847,6 +1857,13 @@ class UpdateAsyncVirtualTagConfig(BaseModel):
1847
1857
  values: Optional[List[VirtualTagConfigValue]] = Field(default=None, description="Values for the VirtualTagConfig, with match precedence determined by order in the list.")
1848
1858
 
1849
1859
 
1860
+ class AsyncVirtualTagConfigUpdate(BaseModel):
1861
+ """AsyncVirtualTagConfigUpdate model"""
1862
+
1863
+ request_id: str = Field(description="The request ID of the async virtual tag config update.")
1864
+ status_url: str = Field(description="The status path of the async virtual tag config update.")
1865
+
1866
+
1850
1867
  class Workspaces(BaseModel):
1851
1868
  """Workspaces model"""
1852
1869
 
File without changes
File without changes
File without changes