wayscloud 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WAYSCloud AS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: wayscloud
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the WAYSCloud API
5
+ Home-page: https://wayscloud.services
6
+ License: MIT
7
+ Project-URL: Homepage, https://wayscloud.services
8
+ Project-URL: Documentation, https://docs.wayscloud.services
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: httpx>=0.25
20
+ Dynamic: license-file
21
+
22
+ # WAYSCloud Python SDK
23
+
24
+ Official Python SDK for the [WAYSCloud](https://wayscloud.services) API.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install wayscloud
30
+ ```
31
+
32
+ ## Authentication
33
+
34
+ ```python
35
+ from wayscloud import WaysCloudClient
36
+
37
+ # Personal Access Token
38
+ client = WaysCloudClient(token="wayscloud_pat_...")
39
+
40
+ # API key
41
+ client = WaysCloudClient(api_key="wayscloud_api_...")
42
+
43
+ # Environment variables (WAYSCLOUD_TOKEN or WAYSCLOUD_API_KEY)
44
+ client = WaysCloudClient()
45
+ ```
46
+
47
+ Priority: explicit arguments > environment variables.
48
+
49
+ ## Usage
50
+
51
+ ```python
52
+ # VPS
53
+ for vm in client.vps.list():
54
+ print(vm["hostname"], vm["status"])
55
+
56
+ # DNS
57
+ client.dns.create_record(
58
+ "example.com",
59
+ record_type="A",
60
+ name="www",
61
+ value="192.0.2.1",
62
+ )
63
+
64
+ # Database
65
+ db = client.database.create(name="prod", db_type="postgresql")
66
+
67
+ # Apps
68
+ app = client.apps.create(name="my-app", region="eu")
69
+
70
+ # Storage
71
+ client.storage.create_bucket("my-bucket")
72
+
73
+ # SMS
74
+ client.sms.send(to="+4712345678", message="Hello from WAYSCloud")
75
+ ```
76
+
77
+ ## Error handling
78
+
79
+ ```python
80
+ from wayscloud import NotFoundError, AuthenticationError
81
+
82
+ try:
83
+ client.vps.get("id")
84
+ except NotFoundError:
85
+ print("Not found")
86
+ except AuthenticationError:
87
+ print("Invalid credentials")
88
+ ```
89
+
90
+ All exceptions inherit from `WaysCloudError`.
91
+
92
+ ## Configuration
93
+
94
+ | Parameter | Environment variable | Default |
95
+ |-----------|---------------------|---------|
96
+ | `token` | `WAYSCLOUD_TOKEN` | — |
97
+ | `api_key` | `WAYSCLOUD_API_KEY` | — |
98
+ | `base_url` | `WAYSCLOUD_API_URL` | `https://api.wayscloud.services` |
99
+ | `timeout` | — | `30.0` |
100
+
101
+ ## Retries
102
+
103
+ Automatic retries on 429, 502, 503, 504 with exponential backoff. Respects `Retry-After` headers.
104
+
105
+ ## Requirements
106
+
107
+ - Python 3.10+
108
+ - httpx
109
+
110
+ ## License
111
+
112
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,91 @@
1
+ # WAYSCloud Python SDK
2
+
3
+ Official Python SDK for the [WAYSCloud](https://wayscloud.services) API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install wayscloud
9
+ ```
10
+
11
+ ## Authentication
12
+
13
+ ```python
14
+ from wayscloud import WaysCloudClient
15
+
16
+ # Personal Access Token
17
+ client = WaysCloudClient(token="wayscloud_pat_...")
18
+
19
+ # API key
20
+ client = WaysCloudClient(api_key="wayscloud_api_...")
21
+
22
+ # Environment variables (WAYSCLOUD_TOKEN or WAYSCLOUD_API_KEY)
23
+ client = WaysCloudClient()
24
+ ```
25
+
26
+ Priority: explicit arguments > environment variables.
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ # VPS
32
+ for vm in client.vps.list():
33
+ print(vm["hostname"], vm["status"])
34
+
35
+ # DNS
36
+ client.dns.create_record(
37
+ "example.com",
38
+ record_type="A",
39
+ name="www",
40
+ value="192.0.2.1",
41
+ )
42
+
43
+ # Database
44
+ db = client.database.create(name="prod", db_type="postgresql")
45
+
46
+ # Apps
47
+ app = client.apps.create(name="my-app", region="eu")
48
+
49
+ # Storage
50
+ client.storage.create_bucket("my-bucket")
51
+
52
+ # SMS
53
+ client.sms.send(to="+4712345678", message="Hello from WAYSCloud")
54
+ ```
55
+
56
+ ## Error handling
57
+
58
+ ```python
59
+ from wayscloud import NotFoundError, AuthenticationError
60
+
61
+ try:
62
+ client.vps.get("id")
63
+ except NotFoundError:
64
+ print("Not found")
65
+ except AuthenticationError:
66
+ print("Invalid credentials")
67
+ ```
68
+
69
+ All exceptions inherit from `WaysCloudError`.
70
+
71
+ ## Configuration
72
+
73
+ | Parameter | Environment variable | Default |
74
+ |-----------|---------------------|---------|
75
+ | `token` | `WAYSCLOUD_TOKEN` | — |
76
+ | `api_key` | `WAYSCLOUD_API_KEY` | — |
77
+ | `base_url` | `WAYSCLOUD_API_URL` | `https://api.wayscloud.services` |
78
+ | `timeout` | — | `30.0` |
79
+
80
+ ## Retries
81
+
82
+ Automatic retries on 429, 502, 503, 504 with exponential backoff. Respects `Retry-After` headers.
83
+
84
+ ## Requirements
85
+
86
+ - Python 3.10+
87
+ - httpx
88
+
89
+ ## License
90
+
91
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "wayscloud"
3
+ version = "0.1.0"
4
+ description = "Official Python SDK for the WAYSCloud API"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ requires-python = ">=3.10"
8
+ dependencies = ["httpx>=0.25"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://wayscloud.services"
21
+ Documentation = "https://docs.wayscloud.services"
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["wayscloud*"]
25
+
26
+ [build-system]
27
+ requires = ["setuptools>=68"]
28
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,33 @@
1
+ [metadata]
2
+ name = wayscloud
3
+ version = 0.1.0
4
+ description = Official Python SDK for the WAYSCloud API
5
+ long_description = file: README.md
6
+ long_description_content_type = text/markdown
7
+ license = MIT
8
+ url = https://wayscloud.services
9
+ classifiers =
10
+ Development Status :: 4 - Beta
11
+ Intended Audience :: Developers
12
+ License :: OSI Approved :: MIT License
13
+ Programming Language :: Python :: 3
14
+ Programming Language :: Python :: 3.10
15
+ Programming Language :: Python :: 3.11
16
+ Programming Language :: Python :: 3.12
17
+
18
+ [options]
19
+ packages = find:
20
+ python_requires = >=3.10
21
+ install_requires =
22
+ httpx>=0.25
23
+
24
+ [options.packages.find]
25
+ include = wayscloud*
26
+
27
+ [options.package_data]
28
+ wayscloud = py.typed
29
+
30
+ [egg_info]
31
+ tag_build =
32
+ tag_date = 0
33
+
@@ -0,0 +1,241 @@
1
+ """SDK smoke tests — verify public API surface, auth, lifecycle, and error handling."""
2
+
3
+ import os
4
+
5
+ import httpx
6
+ import pytest
7
+ import respx
8
+
9
+ from wayscloud import (
10
+ WaysCloudClient,
11
+ WaysCloudError,
12
+ AuthenticationError,
13
+ NotFoundError,
14
+ ValidationError,
15
+ ServerError,
16
+ __version__,
17
+ )
18
+
19
+
20
+ # ── Import & version ────────────────────────────────────────────
21
+
22
+ def test_version_is_string():
23
+ assert isinstance(__version__, str)
24
+ assert "." in __version__
25
+
26
+
27
+ # ── Auth: explicit args ─────────────────────────────────────────
28
+
29
+ def test_token_sets_bearer_header():
30
+ c = WaysCloudClient(token="wayscloud_pat_test123")
31
+ assert c._headers["Authorization"] == "Bearer wayscloud_pat_test123"
32
+ c.close()
33
+
34
+
35
+ def test_api_key_token_sets_x_api_key():
36
+ c = WaysCloudClient(token="wayscloud_api_test123")
37
+ assert c._headers["X-API-Key"] == "wayscloud_api_test123"
38
+ assert "Authorization" not in c._headers
39
+ c.close()
40
+
41
+
42
+ def test_explicit_api_key_sets_header():
43
+ c = WaysCloudClient(api_key="wayscloud_api_abc")
44
+ assert c._headers["X-API-Key"] == "wayscloud_api_abc"
45
+ c.close()
46
+
47
+
48
+ def test_both_token_and_api_key():
49
+ c = WaysCloudClient(token="wayscloud_pat_x", api_key="wayscloud_api_y")
50
+ assert c._headers["Authorization"] == "Bearer wayscloud_pat_x"
51
+ assert c._headers["X-API-Key"] == "wayscloud_api_y"
52
+ c.close()
53
+
54
+
55
+ # ── Auth: env var fallback ──────────────────────────────────────
56
+
57
+ def test_env_var_token(monkeypatch):
58
+ monkeypatch.setenv("WAYSCLOUD_TOKEN", "wayscloud_pat_env")
59
+ monkeypatch.delenv("WAYSCLOUD_API_KEY", raising=False)
60
+ c = WaysCloudClient()
61
+ assert c._headers["Authorization"] == "Bearer wayscloud_pat_env"
62
+ c.close()
63
+
64
+
65
+ def test_env_var_api_key(monkeypatch):
66
+ monkeypatch.delenv("WAYSCLOUD_TOKEN", raising=False)
67
+ monkeypatch.setenv("WAYSCLOUD_API_KEY", "wayscloud_api_env")
68
+ c = WaysCloudClient()
69
+ assert c._headers["X-API-Key"] == "wayscloud_api_env"
70
+ c.close()
71
+
72
+
73
+ def test_env_var_base_url(monkeypatch):
74
+ monkeypatch.setenv("WAYSCLOUD_API_URL", "https://custom.example.com")
75
+ c = WaysCloudClient(token="t")
76
+ assert c.base_url == "https://custom.example.com"
77
+ c.close()
78
+
79
+
80
+ def test_explicit_overrides_env(monkeypatch):
81
+ monkeypatch.setenv("WAYSCLOUD_TOKEN", "wayscloud_pat_env")
82
+ c = WaysCloudClient(token="wayscloud_pat_explicit")
83
+ assert c._headers["Authorization"] == "Bearer wayscloud_pat_explicit"
84
+ c.close()
85
+
86
+
87
+ # ── Client lifecycle ────────────────────────────────────────────
88
+
89
+ def test_context_manager():
90
+ with WaysCloudClient(token="t") as c:
91
+ assert c._http is not None
92
+ assert c._http.is_closed
93
+
94
+
95
+ def test_close():
96
+ c = WaysCloudClient(token="t")
97
+ assert not c._http.is_closed
98
+ c.close()
99
+ assert c._http.is_closed
100
+
101
+
102
+ # ── Service properties ──────────────────────────────────────────
103
+
104
+ def test_all_services_instantiate():
105
+ c = WaysCloudClient(token="t")
106
+ services = [c.vps, c.dns, c.storage, c.database, c.redis, c.apps, c.iot, c.sms, c.account]
107
+ assert len(services) == 9
108
+ assert all(s is not None for s in services)
109
+ # Lazy — same instance on second access
110
+ assert c.vps is c.vps
111
+ c.close()
112
+
113
+
114
+ # ── HTTP: success ───────────────────────────────────────────────
115
+
116
+ @respx.mock
117
+ def test_get_returns_json():
118
+ respx.get("https://api.wayscloud.services/api/v1/dashboard/vps").mock(
119
+ return_value=httpx.Response(200, json={"instances": []})
120
+ )
121
+ with WaysCloudClient(token="t") as c:
122
+ result = c.vps.list()
123
+ assert result == []
124
+
125
+
126
+ @respx.mock
127
+ def test_post_sends_json_body():
128
+ route = respx.post("https://api.wayscloud.services/api/v1/dashboard/dns/zones").mock(
129
+ return_value=httpx.Response(201, json={"id": "z1", "name": "example.com"})
130
+ )
131
+ with WaysCloudClient(token="t") as c:
132
+ result = c.dns.create_zone("example.com")
133
+ assert result["name"] == "example.com"
134
+ assert route.calls[0].request.headers["content-type"] == "application/json"
135
+
136
+
137
+ @respx.mock
138
+ def test_get_does_not_send_content_type():
139
+ route = respx.get("https://api.wayscloud.services/api/v1/dashboard/vps").mock(
140
+ return_value=httpx.Response(200, json={"instances": []})
141
+ )
142
+ with WaysCloudClient(token="t") as c:
143
+ c.vps.list()
144
+ assert "content-type" not in route.calls[0].request.headers
145
+
146
+
147
+ @respx.mock
148
+ def test_204_returns_ok():
149
+ respx.delete("https://api.wayscloud.services/api/v1/dashboard/vps/abc").mock(
150
+ return_value=httpx.Response(204)
151
+ )
152
+ with WaysCloudClient(token="t") as c:
153
+ result = c.vps.delete("abc")
154
+ assert result == {"status": "ok"}
155
+
156
+
157
+ # ── HTTP: error mapping ────────────────────────────────────────
158
+
159
+ @respx.mock
160
+ def test_401_raises_authentication_error():
161
+ respx.get("https://api.wayscloud.services/api/v1/dashboard/vps").mock(
162
+ return_value=httpx.Response(401, json={"detail": "Invalid token"})
163
+ )
164
+ with WaysCloudClient(token="t") as c:
165
+ with pytest.raises(AuthenticationError) as exc:
166
+ c.vps.list()
167
+ assert exc.value.status_code == 401
168
+
169
+
170
+ @respx.mock
171
+ def test_404_raises_not_found():
172
+ respx.get("https://api.wayscloud.services/api/v1/dashboard/vps/bad").mock(
173
+ return_value=httpx.Response(404, json={"detail": "Not found"})
174
+ )
175
+ with WaysCloudClient(token="t") as c:
176
+ with pytest.raises(NotFoundError):
177
+ c.vps.get("bad")
178
+
179
+
180
+ @respx.mock
181
+ def test_422_raises_validation_error():
182
+ respx.post("https://api.wayscloud.services/api/v1/dashboard/dns/zones").mock(
183
+ return_value=httpx.Response(422, json={"detail": "Invalid zone name"})
184
+ )
185
+ with WaysCloudClient(token="t") as c:
186
+ with pytest.raises(ValidationError):
187
+ c.dns.create_zone("")
188
+
189
+
190
+ @respx.mock
191
+ def test_500_raises_server_error():
192
+ respx.get("https://api.wayscloud.services/api/v1/dashboard/vps").mock(
193
+ return_value=httpx.Response(500, json={"detail": "Internal error"})
194
+ )
195
+ with WaysCloudClient(token="t") as c:
196
+ with pytest.raises(ServerError):
197
+ c.vps.list()
198
+
199
+
200
+ # ── HTTP: retry ─────────────────────────────────────────────────
201
+
202
+ @respx.mock
203
+ def test_retries_on_429_then_succeeds():
204
+ route = respx.get("https://api.wayscloud.services/api/v1/dashboard/vps").mock(
205
+ side_effect=[
206
+ httpx.Response(429),
207
+ httpx.Response(200, json={"instances": [{"hostname": "ok"}]}),
208
+ ]
209
+ )
210
+ with WaysCloudClient(token="t") as c:
211
+ c.BACKOFF_FACTOR = 0.01 # Speed up test
212
+ result = c.vps.list()
213
+ assert len(result) == 1
214
+ assert route.call_count == 2
215
+
216
+
217
+ # ── Service method signatures ───────────────────────────────────
218
+
219
+ def test_dns_create_record_uses_record_type():
220
+ """Verify renamed param — no Python builtin shadow."""
221
+ import inspect
222
+ from wayscloud.services.dns import DNSService
223
+ sig = inspect.signature(DNSService.create_record)
224
+ assert "record_type" in sig.parameters
225
+ assert "type" not in sig.parameters
226
+
227
+
228
+ def test_database_create_uses_db_type():
229
+ import inspect
230
+ from wayscloud.services.database import DatabaseService
231
+ sig = inspect.signature(DatabaseService.create)
232
+ assert "db_type" in sig.parameters
233
+ assert "type" not in sig.parameters
234
+
235
+
236
+ def test_iot_create_rule_uses_rule_type():
237
+ import inspect
238
+ from wayscloud.services.iot import IoTService
239
+ sig = inspect.signature(IoTService.create_rule)
240
+ assert "rule_type" in sig.parameters
241
+ assert "type" not in sig.parameters
@@ -0,0 +1,5 @@
1
+ from .client import WaysCloudClient
2
+ from .exceptions import WaysCloudError, AuthenticationError, NotFoundError, ValidationError, ServerError
3
+ from ._version import __version__
4
+
5
+ __all__ = ["WaysCloudClient", "WaysCloudError", "AuthenticationError", "NotFoundError", "ValidationError", "ServerError", "__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"