mtn-cloud 0.3.1__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.
Files changed (84) hide show
  1. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/PKG-INFO +30 -17
  2. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/README.md +27 -12
  3. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/advanced-cookbook.md +15 -4
  4. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-overview.md +5 -4
  5. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/client.md +17 -3
  6. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/instances.md +25 -0
  7. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/index.md +8 -6
  8. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/instances.md +35 -6
  9. mtn_cloud-0.3.3/docs/recipes.md +110 -0
  10. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/pyproject.toml +3 -4
  11. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/_version.py +1 -1
  12. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/client.py +9 -8
  13. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/config.py +28 -8
  14. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/http.py +177 -41
  15. mtn_cloud-0.3.3/src/mtn_cloud/py.typed +0 -0
  16. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/instances.py +185 -1
  17. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_client.py +35 -2
  18. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_config.py +40 -4
  19. mtn_cloud-0.3.3/tests/test_http.py +335 -0
  20. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_instances.py +110 -0
  21. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/.gitignore +0 -0
  22. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/LICENSE +0 -0
  23. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/archive-buckets.md +0 -0
  24. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/backups.md +0 -0
  25. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/clouds.md +0 -0
  26. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/groups.md +0 -0
  27. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/index.md +0 -0
  28. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/instance-types.md +0 -0
  29. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/networks.md +0 -0
  30. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/plans.md +0 -0
  31. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/security-groups.md +0 -0
  32. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/storage-buckets.md +0 -0
  33. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/api-reference/virtual-images.md +0 -0
  34. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/backups.md +0 -0
  35. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/networking.md +0 -0
  36. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/quickstart.md +0 -0
  37. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/security-groups.md +0 -0
  38. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/storage.md +0 -0
  39. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/stylesheets/extra.css +0 -0
  40. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/docs/virtual-images.md +0 -0
  41. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/__init__.py +0 -0
  42. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/exceptions.py +0 -0
  43. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/__init__.py +0 -0
  44. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/archive.py +0 -0
  45. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/backup.py +0 -0
  46. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/base.py +0 -0
  47. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/cloud.py +0 -0
  48. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/group.py +0 -0
  49. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/instance.py +0 -0
  50. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/instance_type.py +0 -0
  51. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/network.py +0 -0
  52. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/plan.py +0 -0
  53. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/resource_pool.py +0 -0
  54. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/security_group.py +0 -0
  55. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/snapshot.py +0 -0
  56. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/storage_bucket.py +0 -0
  57. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/user.py +0 -0
  58. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/virtual_image.py +0 -0
  59. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/models/volume.py +0 -0
  60. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/__init__.py +0 -0
  61. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/archive_buckets.py +0 -0
  62. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/backups.py +0 -0
  63. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/base.py +0 -0
  64. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/clouds.py +0 -0
  65. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/groups.py +0 -0
  66. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/instance_types.py +0 -0
  67. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/networks.py +0 -0
  68. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/plans.py +0 -0
  69. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/security_groups.py +0 -0
  70. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/storage_buckets.py +0 -0
  71. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/src/mtn_cloud/resources/virtual_images.py +0 -0
  72. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/__init__.py +0 -0
  73. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/conftest.py +0 -0
  74. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_archive_buckets.py +0 -0
  75. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_backups.py +0 -0
  76. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_clouds.py +0 -0
  77. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_exceptions.py +0 -0
  78. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_groups.py +0 -0
  79. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_instance_types.py +0 -0
  80. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_networks.py +0 -0
  81. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_plans.py +0 -0
  82. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_security_groups.py +0 -0
  83. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_storage_buckets.py +0 -0
  84. {mtn_cloud-0.3.1 → mtn_cloud-0.3.3}/tests/test_virtual_images.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: mtn-cloud
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Community Python SDK for MTN Cloud - Deploy and manage cloud resources with ease
5
5
  Project-URL: Homepage, https://github.com/mahveotm/mtn-cloud-python
6
6
  Project-URL: Documentation, https://github.com/mahveotm/mtn-cloud-python#readme
@@ -10,7 +10,7 @@ Author-email: Marvellous Osuolale <m@rvellous.com>
10
10
  Maintainer-email: Marvellous Osuolale <m@rvellous.com>
11
11
  License: MIT
12
12
  License-File: LICENSE
13
- Keywords: api,cloud,devops,infrastructure,mtn,mtn-cloud,sdk
13
+ Keywords: api,cloud,cloud-management,devops,infrastructure,mtn,mtn-cloud,mtn-cloud-python,sdk
14
14
  Classifier: Development Status :: 4 - Beta
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: License :: OSI Approved :: MIT License
@@ -31,10 +31,8 @@ Requires-Dist: typing-extensions>=4.5.0
31
31
  Requires-Dist: urllib3>=2.0.0
32
32
  Provides-Extra: dev
33
33
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
34
- Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
35
34
  Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
36
35
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
37
- Requires-Dist: respx>=0.20.0; extra == 'dev'
38
36
  Requires-Dist: ruff>=0.1.0; extra == 'dev'
39
37
  Requires-Dist: types-requests>=2.28.0; extra == 'dev'
40
38
  Provides-Extra: docs
@@ -150,33 +148,36 @@ for plan in cloud.instances.list_service_plans(
150
148
  print(plan["id"], plan["name"])
151
149
  ```
152
150
 
153
- A **resource pool is required** to create an instance — it's where the VM is hosted. Discover it by group name (the cloud/zone is resolved for you), or fetch one directly by name:
151
+ A **resource pool is required** to create an instance — it's where the VM is hosted. `provision()` (below) discovers it for you; or fetch one yourself with `cloud.instances.get_resource_pool("my-project", group="MTNNG_CLOUD_AZ_1")`.
152
+
153
+ ### 2. Create an Instance
154
+
155
+ The guided `provision()` resolves layout, plan, and resource pool from names — the fastest path:
154
156
 
155
157
  ```python
156
- pool = cloud.instances.get_resource_pool(
157
- "my-project-name",
158
- group="MTNNG_CLOUD_AZ_1",
158
+ instance = cloud.instances.provision(
159
+ name="web-01",
160
+ type="MTN-CS10", # type code
161
+ group="MTNNG_CLOUD_AZ_1", # also used as the cloud/zone
162
+ plan="G2S4", # plan name (or numeric id)
163
+ resource_pool="my-project", # pool name; auto-selected if the group has only one
159
164
  )
160
- print(pool.code) # e.g. "pool-214" — pass this as resource_pool_id
165
+ print(instance.id, instance.status, instance.primary_ip)
161
166
  ```
162
167
 
163
- ### 2. Create an Instance
168
+ For full control over every ID, use `create()` directly:
164
169
 
165
170
  ```python
166
- pool = cloud.instances.get_resource_pool("my-project-name", group="MTNNG_CLOUD_AZ_1")
167
-
168
171
  instance = cloud.instances.create(
169
- name="my-server",
172
+ name="web-01",
170
173
  cloud="MTNNG_CLOUD_AZ_1",
171
174
  type="MTN-CS10",
172
175
  group="MTNNG_CLOUD_AZ_1",
173
176
  layout=327,
174
177
  plan=6776,
175
- resource_pool_id=pool.code, # "pool-214" — or pass the numeric ID 214
178
+ resource_pool_id="pool-214", # or the numeric ID 214
176
179
  labels=["production", "web"],
177
180
  )
178
-
179
- print(f"Created: {instance.id} {instance.name} ({instance.status})")
180
181
  ```
181
182
 
182
183
  ### 3. Manage an Instance
@@ -404,6 +405,8 @@ Environment variables:
404
405
  | `MTN_CLOUD_MAX_RETRIES` | Maximum retry attempts | `3` |
405
406
  | `MTN_CLOUD_RETRY_DELAY` | Retry backoff factor | `1.0` |
406
407
  | `MTN_CLOUD_VERIFY_SSL` | Enable SSL verification | `true` |
408
+ | `MTN_CLOUD_USER_AGENT` | Application identity appended to the SDK user agent | - |
409
+ | `MTN_CLOUD_DEBUG` | Enable bounded, secret-redacted HTTP debug logs | `false` |
407
410
 
408
411
  Programmatic configuration:
409
412
 
@@ -416,11 +419,21 @@ config = MTNCloudConfig(
416
419
  max_retries=5,
417
420
  retry_delay=1.5,
418
421
  verify_ssl=True,
422
+ user_agent="my-automation/1.0", # appended after the required SDK identity
419
423
  )
420
424
 
421
425
  cloud = MTNCloud(config=config)
422
426
  ```
423
427
 
428
+ Tokens and passwords are stored as masked Pydantic secret values, and debug
429
+ logging recursively redacts credential-shaped fields. The transport retries
430
+ transient failures only for safe read methods (`GET`, `HEAD`, and `OPTIONS`);
431
+ create, update, action, and delete requests are never status-retried automatically.
432
+
433
+ The default `mtn-cloud-python/<version>` user-agent prefix is required by the
434
+ MTN Cloud API edge. A configured `user_agent` is treated as an application
435
+ suffix so the SDK identity is preserved.
436
+
424
437
  ## Examples
425
438
 
426
439
  | Script | What it demonstrates |
@@ -106,33 +106,36 @@ for plan in cloud.instances.list_service_plans(
106
106
  print(plan["id"], plan["name"])
107
107
  ```
108
108
 
109
- A **resource pool is required** to create an instance — it's where the VM is hosted. Discover it by group name (the cloud/zone is resolved for you), or fetch one directly by name:
109
+ A **resource pool is required** to create an instance — it's where the VM is hosted. `provision()` (below) discovers it for you; or fetch one yourself with `cloud.instances.get_resource_pool("my-project", group="MTNNG_CLOUD_AZ_1")`.
110
+
111
+ ### 2. Create an Instance
112
+
113
+ The guided `provision()` resolves layout, plan, and resource pool from names — the fastest path:
110
114
 
111
115
  ```python
112
- pool = cloud.instances.get_resource_pool(
113
- "my-project-name",
114
- group="MTNNG_CLOUD_AZ_1",
116
+ instance = cloud.instances.provision(
117
+ name="web-01",
118
+ type="MTN-CS10", # type code
119
+ group="MTNNG_CLOUD_AZ_1", # also used as the cloud/zone
120
+ plan="G2S4", # plan name (or numeric id)
121
+ resource_pool="my-project", # pool name; auto-selected if the group has only one
115
122
  )
116
- print(pool.code) # e.g. "pool-214" — pass this as resource_pool_id
123
+ print(instance.id, instance.status, instance.primary_ip)
117
124
  ```
118
125
 
119
- ### 2. Create an Instance
126
+ For full control over every ID, use `create()` directly:
120
127
 
121
128
  ```python
122
- pool = cloud.instances.get_resource_pool("my-project-name", group="MTNNG_CLOUD_AZ_1")
123
-
124
129
  instance = cloud.instances.create(
125
- name="my-server",
130
+ name="web-01",
126
131
  cloud="MTNNG_CLOUD_AZ_1",
127
132
  type="MTN-CS10",
128
133
  group="MTNNG_CLOUD_AZ_1",
129
134
  layout=327,
130
135
  plan=6776,
131
- resource_pool_id=pool.code, # "pool-214" — or pass the numeric ID 214
136
+ resource_pool_id="pool-214", # or the numeric ID 214
132
137
  labels=["production", "web"],
133
138
  )
134
-
135
- print(f"Created: {instance.id} {instance.name} ({instance.status})")
136
139
  ```
137
140
 
138
141
  ### 3. Manage an Instance
@@ -360,6 +363,8 @@ Environment variables:
360
363
  | `MTN_CLOUD_MAX_RETRIES` | Maximum retry attempts | `3` |
361
364
  | `MTN_CLOUD_RETRY_DELAY` | Retry backoff factor | `1.0` |
362
365
  | `MTN_CLOUD_VERIFY_SSL` | Enable SSL verification | `true` |
366
+ | `MTN_CLOUD_USER_AGENT` | Application identity appended to the SDK user agent | - |
367
+ | `MTN_CLOUD_DEBUG` | Enable bounded, secret-redacted HTTP debug logs | `false` |
363
368
 
364
369
  Programmatic configuration:
365
370
 
@@ -372,11 +377,21 @@ config = MTNCloudConfig(
372
377
  max_retries=5,
373
378
  retry_delay=1.5,
374
379
  verify_ssl=True,
380
+ user_agent="my-automation/1.0", # appended after the required SDK identity
375
381
  )
376
382
 
377
383
  cloud = MTNCloud(config=config)
378
384
  ```
379
385
 
386
+ Tokens and passwords are stored as masked Pydantic secret values, and debug
387
+ logging recursively redacts credential-shaped fields. The transport retries
388
+ transient failures only for safe read methods (`GET`, `HEAD`, and `OPTIONS`);
389
+ create, update, action, and delete requests are never status-retried automatically.
390
+
391
+ The default `mtn-cloud-python/<version>` user-agent prefix is required by the
392
+ MTN Cloud API edge. A configured `user_agent` is treated as an application
393
+ suffix so the SDK identity is preserved.
394
+
380
395
  ## Examples
381
396
 
382
397
  | Script | What it demonstrates |
@@ -5,14 +5,16 @@ Production-friendly patterns for robust automation.
5
5
  ## Configure a Resilient Client
6
6
 
7
7
  ```python
8
- from mtn_cloud import MTNCloud
8
+ from mtn_cloud import MTNCloud, MTNCloudConfig
9
9
 
10
- cloud = MTNCloud(
10
+ config = MTNCloudConfig(
11
11
  token="your-api-token",
12
12
  timeout=45,
13
- # Retry values are controlled through MTNCloudConfig defaults:
14
- # max_retries=3, retry_delay=1.0
13
+ max_retries=3,
14
+ retry_delay=1.0,
15
+ user_agent="my-automation/1.0",
15
16
  )
17
+ cloud = MTNCloud(config=config)
16
18
  ```
17
19
 
18
20
  Use environment variables for deployment:
@@ -24,6 +26,15 @@ export MTN_CLOUD_MAX_RETRIES="5"
24
26
  export MTN_CLOUD_RETRY_DELAY="1.5"
25
27
  ```
26
28
 
29
+ Retries apply only to safe reads (`GET`, `HEAD`, and `OPTIONS`). The SDK does
30
+ not automatically retry provisioning, actions, updates, or deletes because a
31
+ server or proxy error does not prove that a write was rejected before execution.
32
+
33
+ `user_agent` adds an application identity after the SDK's required
34
+ `mtn-cloud-python/<version>` prefix. Debug logging is bounded and recursively
35
+ redacts tokens, passwords, access keys, secret keys, cookies, authorization
36
+ values, and similar credential fields.
37
+
27
38
  ## Pattern: Idempotent "Get or Create"
28
39
 
29
40
  ```python
@@ -42,10 +42,11 @@ cloud.instance_types.get_by_code("MTN-CS10")
42
42
  ## Common Write Operations
43
43
 
44
44
  ```python
45
- # Discover the resource pool an instance will be hosted in
46
- pool = cloud.instances.get_resource_pool("my-project", group="MTNNG_CLOUD_AZ_1")
47
-
48
- cloud.instances.create(..., resource_pool_id=pool.code)
45
+ # Guided: resolves layout, plan, and resource pool from names
46
+ cloud.instances.provision(name="web-01", type="MTN-CS10",
47
+ group="MTNNG_CLOUD_AZ_1", plan="G2S4", resource_pool="my-project")
48
+ # Explicit: full control over each ID
49
+ cloud.instances.create(..., resource_pool_id="pool-214")
49
50
  cloud.instances.update(instance_id=123, name="new-name")
50
51
  cloud.instances.delete(123)
51
52
 
@@ -1,6 +1,6 @@
1
1
  # Client: `MTNCloud`
2
2
 
3
- ### `MTNCloud(token=None, username=None, password=None, url=None, timeout=None, verify_ssl=True, config=None)`
3
+ ### `MTNCloud(token=None, username=None, password=None, url=None, timeout=None, verify_ssl=None, config=None)`
4
4
 
5
5
  Creates an SDK client and lazy-loads resource managers.
6
6
 
@@ -10,12 +10,27 @@ Creates an SDK client and lazy-loads resource managers.
10
10
  - `username`, `password`: OAuth password-grant alternative
11
11
  - `url`: MTN Cloud base console URL, no `/api` needed
12
12
  - `timeout`: request timeout in seconds
13
- - `verify_ssl`: enable/disable TLS certificate validation
13
+ - `verify_ssl`: enable/disable TLS certificate validation; when omitted,
14
+ `MTN_CLOUD_VERIFY_SSL` or the configuration default is used
14
15
  - `config`: explicit `MTNCloudConfig` object (overrides other args)
15
16
  - Returns: `MTNCloud`
16
17
  - Raises:
17
18
  - `pydantic.ValidationError` if config values violate `MTNCloudConfig` constraints
18
19
 
20
+ Tokens and passwords are stored as masked secret values. Use
21
+ `config.get_token_value()` or `config.get_password_value()` only when an explicit
22
+ integration needs the underlying value; normal SDK usage never needs to reveal
23
+ them.
24
+
25
+ The transport:
26
+
27
+ - preserves the `mtn-cloud-python/<version>` user-agent identity required by the
28
+ MTN API edge;
29
+ - recursively redacts secrets from debug logs and raised error response payloads;
30
+ - retries 429 and transient 5xx responses only for `GET`, `HEAD`, and `OPTIONS`;
31
+ - does not automatically status-retry `POST`, `PUT`, `PATCH`, or `DELETE` calls;
32
+ - handles both delta-seconds and HTTP-date `Retry-After` headers.
33
+
19
34
  ### `whoami() -> User`
20
35
 
21
36
  - Endpoint: `GET /api/whoami`
@@ -41,4 +56,3 @@ Connectivity/auth convenience check.
41
56
  - Parameters: none
42
57
  - Returns: `None`
43
58
  - Raises: none
44
-
@@ -61,6 +61,31 @@
61
61
  - common API exceptions
62
62
  - `NotFoundError` when `group` cannot be resolved
63
63
 
64
+ ### `provision(name: str, *, type: str, group: str, plan: str | int, resource_pool=None, cloud=None, availability_zone=None, security_group="default", wait=True, timeout=600, dry_run=False, **create_kwargs) -> Instance | dict`
65
+
66
+ Guided wrapper over `create()` that resolves IDs from names.
67
+
68
+ - Endpoint sequence:
69
+ - `GET /api/groups?name=<group>` (resolve group → `cloud_ids`)
70
+ - `GET /api/instance-types?code=<type>` (resolve `layout` via `default_layout_id`)
71
+ - `GET /api/instances/service-plans` (resolve `plan` name → ID, unless `plan` is an int)
72
+ - `GET /api/options/zonePools` (resolve/auto-select `resource_pool`, unless given as int)
73
+ - `POST /api/instances`, then polls `GET /api/instances/{id}` when `wait=True`
74
+ - Parameters:
75
+ - `type`: instance type code; its `default_layout_id` becomes `layout`
76
+ - `group`: group name; also used as `cloud` unless `cloud` is given
77
+ - `plan`: plan name (resolved against the live list) or numeric ID
78
+ - `resource_pool`: pool name, code, or numeric ID. If omitted, the group's single pool is used; if it has several, a `ValidationError` lists them
79
+ - `cloud`: cloud/zone name; defaults to `group`
80
+ - `wait`: block until running (default `True`)
81
+ - `timeout`: max seconds to wait when `wait=True`
82
+ - `dry_run`: return the resolved config dict instead of creating
83
+ - `**create_kwargs`: forwarded to `create()` (labels, tags, volumes, network_interfaces, ports, …)
84
+ - Returns: the created `Instance` (running if `wait=True`), or a config `dict` when `dry_run=True`
85
+ - Raises:
86
+ - `NotFoundError` when the group or instance type can't be resolved
87
+ - `ValidationError` when the plan or resource pool can't be resolved
88
+
64
89
  ### `update(instance_id: int, name=None, description=None, labels=None) -> Instance`
65
90
 
66
91
  - Endpoint: `PUT /api/instances/{instance_id}`
@@ -15,9 +15,10 @@ Use this guide in two ways:
15
15
  5. [Security Groups](./security-groups.md)
16
16
  6. [Backups](./backups.md)
17
17
  7. [Virtual Images](./virtual-images.md)
18
- 8. [Advanced Cookbook](./advanced-cookbook.md)
19
- 9. [API Overview](./api-overview.md)
20
- 10. [API Reference](./api-reference/index.md)
18
+ 8. [Recipes](./recipes.md)
19
+ 9. [Advanced Cookbook](./advanced-cookbook.md)
20
+ 10. [API Overview](./api-overview.md)
21
+ 11. [API Reference](./api-reference/index.md)
21
22
 
22
23
  ## Who This Is For
23
24
 
@@ -41,6 +42,7 @@ Use this guide in two ways:
41
42
  5. Security Groups
42
43
  6. Backups
43
44
  7. Virtual Images
44
- 8. API Overview
45
- 9. API Reference
46
- 10. Advanced Cookbook
45
+ 8. Recipes
46
+ 9. API Overview
47
+ 10. API Reference
48
+ 11. Advanced Cookbook
@@ -106,15 +106,44 @@ If you already know the cloud and group IDs, pass them explicitly to skip the gr
106
106
  pools = cloud.instances.list_resource_pools(cloud_id=4, group_id=621)
107
107
  ```
108
108
 
109
- ## Create an Instance
109
+ ## Provision an Instance (guided)
110
+
111
+ `provision()` is the fastest way to create an instance: it takes the names you already know and resolves the numeric IDs for you — the layout from the type code, the plan from its name, and the resource pool from its name (or auto-selects it when the group has exactly one).
110
112
 
111
113
  ```python
112
- # Resolve the resource pool once, then use its code directly
113
- pool = cloud.instances.get_resource_pool(
114
- "my-project-Marv-Osuolale",
115
- group="MTNNG_CLOUD_AZ_1",
114
+ instance = cloud.instances.provision(
115
+ name="app-server-01",
116
+ type="MTN-CS10", # type code -> resolves the layout
117
+ group="MTNNG_CLOUD_AZ_1", # also used as the cloud/zone
118
+ plan="G2S4", # plan name (or pass a numeric plan ID)
119
+ resource_pool="my-project", # pool name; omit if the group has one pool
120
+ availability_zone="Lagos-AZ-1-fd1",
121
+ security_group="default",
116
122
  )
117
123
 
124
+ # wait=True by default, so the instance is already running here
125
+ print(instance.id, instance.name, instance.status, instance.primary_ip)
126
+ ```
127
+
128
+ If a name can't be resolved, the error tells you what's available (e.g. the valid plan names, or the pools to choose from). Set `wait=False` to return immediately, or `dry_run=True` to get the resolved config back without creating anything:
129
+
130
+ ```python
131
+ config = cloud.instances.provision(
132
+ name="app-server-01", type="MTN-CS10", group="MTNNG_CLOUD_AZ_1",
133
+ plan="G2S4", resource_pool="my-project", dry_run=True,
134
+ )
135
+ # {"layout": 327, "plan": 6776, "resource_pool_id": "pool-214", ...}
136
+ ```
137
+
138
+ Any extra keyword (labels, tags, volumes, network_interfaces, ports, …) passes straight through to `create()`.
139
+
140
+ ## Create an Instance (explicit)
141
+
142
+ When you want full control over each ID, call `create()` directly. Resolve the inputs yourself first (see [Discover Required IDs](#discover-required-ids-and-codes)):
143
+
144
+ ```python
145
+ pool = cloud.instances.get_resource_pool("my-project", group="MTNNG_CLOUD_AZ_1")
146
+
118
147
  instance = cloud.instances.create(
119
148
  name="app-server-01",
120
149
  cloud="MTNNG_CLOUD_AZ_1",
@@ -124,7 +153,7 @@ instance = cloud.instances.create(
124
153
  plan=6776, # use a plan ID from list_service_plans()
125
154
  resource_pool_id=pool.code, # e.g. "pool-214" (or pass 214 directly)
126
155
  availability_zone="Lagos-AZ-1-fd1",
127
- security_group="default", # security group name from the console
156
+ security_group="default",
128
157
  )
129
158
 
130
159
  print(instance.id, instance.name, instance.status, instance.primary_ip)
@@ -0,0 +1,110 @@
1
+ # Recipes
2
+
3
+ Three complete, real-world workflows. Each assumes an authenticated client:
4
+
5
+ ```python
6
+ from mtn_cloud import MTNCloud
7
+
8
+ cloud = MTNCloud(token="your-api-token")
9
+ ```
10
+
11
+ For isolated patterns (retries, pagination, idempotency), see the [Advanced Cookbook](./advanced-cookbook.md).
12
+
13
+ ## Provision a Linux VM with SSH
14
+
15
+ Discover the inputs, open SSH in a security group, provision, and wait until it's reachable.
16
+
17
+ ```python
18
+ group = cloud.groups.get_by_name("MTNNG_CLOUD_AZ_1")
19
+ itype = cloud.instance_types.get_by_code("MTN-CS10")
20
+ pool = cloud.instances.get_resource_pool("my-project", group=group.name)
21
+ plans = cloud.instances.list_service_plans(
22
+ zone_id=group.cloud_ids[0], layout_id=itype.default_layout_id, group_id=group.id,
23
+ )
24
+
25
+ sg = cloud.security_groups.create(name="linux-ssh", description="SSH access")
26
+ cloud.security_groups.create_rule(
27
+ sg.id, name="allow-ssh", direction="ingress", protocol="tcp",
28
+ port_range="22", source_type="cidr", source="200.200.113.15/32",
29
+ )
30
+
31
+ instance = cloud.instances.create(
32
+ name="linux-web-01",
33
+ cloud=group.name,
34
+ type=itype.code,
35
+ group=group.name,
36
+ layout=itype.default_layout_id,
37
+ plan=plans[0]["id"],
38
+ resource_pool_id=pool.code,
39
+ availability_zone="Lagos-AZ-1-fd1",
40
+ security_group=sg.name,
41
+ create_user=True,
42
+ )
43
+ instance = cloud.instances.wait_until_running(instance.id, timeout=600)
44
+ print("ready:", instance.primary_ip)
45
+ ```
46
+
47
+ SSH **keys** are uploaded once in the console (**User Settings → SSH Keys**); the SDK controls the firewall rule and user creation. Then: `ssh <your-user>@<instance.primary_ip>`.
48
+
49
+ ## Back up a folder to MTN Object Storage
50
+
51
+ Register the provider and bucket idempotently, preview with a dry run, then upload.
52
+
53
+ ```python
54
+ from mtn_cloud import NotFoundError
55
+
56
+ try:
57
+ storage = cloud.storage_buckets.get_by_name("my-s3-storage")
58
+ except NotFoundError:
59
+ storage = cloud.storage_buckets.create_s3(
60
+ name="my-s3-storage",
61
+ bucket_name="my-app-objects",
62
+ access_key="your-access-key", # from MOS email
63
+ secret_key="your-secret-key", # from MOS email
64
+ endpoint="https://ps1csp-s3.ict.mtn.com.ng:9021",
65
+ create_bucket=True,
66
+ )
67
+
68
+ try:
69
+ archive = cloud.archive_buckets.get_by_name("my-app-archives")
70
+ except NotFoundError:
71
+ archive = cloud.archive_buckets.create(
72
+ name="my-app-archives", storage_provider_id=storage.id, visibility="private",
73
+ )
74
+
75
+ preview = cloud.archive_buckets.upload_directory(
76
+ bucket_name=archive.name, remote_path="/backups/",
77
+ local_directory="./data", recursive=True, dry_run=True,
78
+ )
79
+ if preview.skipped_count:
80
+ raise RuntimeError(f"{preview.skipped_count} files would be skipped")
81
+
82
+ result = cloud.archive_buckets.upload_directory(
83
+ bucket_name=archive.name, remote_path="/backups/",
84
+ local_directory="./data", recursive=True, strict=True,
85
+ )
86
+ print(f"uploaded={result.uploaded_count} failed={result.failed_count}")
87
+ ```
88
+
89
+ ## Tear down demo resources
90
+
91
+ Delete in dependency order — instances first, then the firewall and network they used. Deletes are best-effort so one failure doesn't block the rest.
92
+
93
+ ```python
94
+ from mtn_cloud import MTNCloudError
95
+
96
+ def cleanup(manager, items, label):
97
+ for item in items:
98
+ if (item.name or "").startswith("demo-"):
99
+ try:
100
+ manager.delete(item.id)
101
+ print("deleted", label, item.id)
102
+ except MTNCloudError as exc:
103
+ print("skip", label, item.id, exc)
104
+
105
+ cleanup(cloud.instances, cloud.instances.list(max_results=100), "instance")
106
+ cleanup(cloud.security_groups, cloud.security_groups.list(), "security-group")
107
+ cleanup(cloud.networks, cloud.networks.list(), "network")
108
+ ```
109
+
110
+ A network or security group can't be deleted while an instance still references it — always remove instances first and let them finish.
@@ -21,7 +21,9 @@ keywords = [
21
21
  "sdk",
22
22
  "api",
23
23
  "infrastructure",
24
- "devops"
24
+ "devops",
25
+ "cloud-management",
26
+ "mtn-cloud-python"
25
27
  ]
26
28
  classifiers = [
27
29
  "Development Status :: 4 - Beta",
@@ -50,8 +52,6 @@ dependencies = [
50
52
  dev = [
51
53
  "pytest>=7.0.0",
52
54
  "pytest-cov>=4.0.0",
53
- "pytest-asyncio>=0.21.0",
54
- "respx>=0.20.0",
55
55
  "ruff>=0.1.0",
56
56
  "mypy>=1.0.0",
57
57
  "types-requests>=2.28.0",
@@ -121,4 +121,3 @@ ignore_missing_imports = true
121
121
 
122
122
  [tool.pytest.ini_options]
123
123
  testpaths = ["tests"]
124
- asyncio_mode = "auto"
@@ -1,3 +1,3 @@
1
1
  """Package version metadata."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.3.3"
@@ -82,7 +82,7 @@ class MTNCloud:
82
82
  password: str | None = None,
83
83
  url: str | None = None,
84
84
  timeout: float | None = None,
85
- verify_ssl: bool = True,
85
+ verify_ssl: bool | None = None,
86
86
  config: MTNCloudConfig | None = None,
87
87
  ) -> None:
88
88
  """
@@ -94,7 +94,8 @@ class MTNCloud:
94
94
  password: Password for authentication
95
95
  url: API base URL (default: https://console.cloud.mtn.ng)
96
96
  timeout: Request timeout in seconds
97
- verify_ssl: Verify SSL certificates
97
+ verify_ssl: Verify SSL certificates. When omitted, configuration and
98
+ environment settings are respected.
98
99
  config: Full configuration object (overrides other args)
99
100
  """
100
101
  if config:
@@ -102,17 +103,17 @@ class MTNCloud:
102
103
  else:
103
104
  config_kwargs: dict[str, Any] = {}
104
105
 
105
- if token:
106
+ if token is not None:
106
107
  config_kwargs["token"] = token
107
- if username:
108
+ if username is not None:
108
109
  config_kwargs["username"] = username
109
- if password:
110
+ if password is not None:
110
111
  config_kwargs["password"] = password
111
- if url:
112
+ if url is not None:
112
113
  config_kwargs["url"] = url
113
- if timeout:
114
+ if timeout is not None:
114
115
  config_kwargs["timeout"] = timeout
115
- if not verify_ssl:
116
+ if verify_ssl is not None:
116
117
  config_kwargs["verify_ssl"] = verify_ssl
117
118
 
118
119
  self._config = MTNCloudConfig(**config_kwargs)