uptimer-python-sdk 0.2.0__py3-none-any.whl
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.
- uptimer/__init__.py +3 -0
- uptimer/client.py +21 -0
- uptimer/endpoints/__init__.py +0 -0
- uptimer/endpoints/endpoint.py +38 -0
- uptimer/endpoints/regions.py +24 -0
- uptimer/endpoints/rules.py +75 -0
- uptimer/endpoints/v1.py +17 -0
- uptimer/endpoints/workspaces.py +24 -0
- uptimer/errors.py +25 -0
- uptimer/http.py +52 -0
- uptimer/models/__init__.py +47 -0
- uptimer/models/deserialize.py +121 -0
- uptimer/models/errors.py +44 -0
- uptimer/models/region.py +9 -0
- uptimer/models/rule.py +57 -0
- uptimer/models/workspace.py +9 -0
- uptimer/py.typed +0 -0
- uptimer_python_sdk-0.2.0.dist-info/METADATA +167 -0
- uptimer_python_sdk-0.2.0.dist-info/RECORD +22 -0
- uptimer_python_sdk-0.2.0.dist-info/WHEEL +4 -0
- uptimer_python_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
- uptimer_python_sdk-0.2.0.dist-info/licenses/NOTICE +18 -0
uptimer/__init__.py
ADDED
uptimer/client.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from uptimer.endpoints.v1 import V1Endpoint
|
|
6
|
+
from uptimer.http import UptimerHttpLib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UptimerClient:
|
|
10
|
+
v1: V1Endpoint
|
|
11
|
+
|
|
12
|
+
def __init__(self, api_key: str, base_url: str | None = None):
|
|
13
|
+
self.set_uptimer_http_lib(UptimerHttpLib(api_key, base_url))
|
|
14
|
+
self.v1 = V1Endpoint(self._http_lib)
|
|
15
|
+
|
|
16
|
+
def version(self) -> str:
|
|
17
|
+
response = self._http_lib.client.get(self._http_lib.build_url("version"))
|
|
18
|
+
return cast("str", self._http_lib.parse_response(response=response))
|
|
19
|
+
|
|
20
|
+
def set_uptimer_http_lib(self, http_lib: UptimerHttpLib) -> None:
|
|
21
|
+
self._http_lib = http_lib
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from uptimer.http import UptimerHttpLib
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseEndpoint:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
http: UptimerHttpLib,
|
|
13
|
+
segment: str,
|
|
14
|
+
parent_segments: str | list[str] | None = None,
|
|
15
|
+
):
|
|
16
|
+
self._http = http
|
|
17
|
+
self._segment = segment
|
|
18
|
+
if parent_segments is None:
|
|
19
|
+
parent_segments = []
|
|
20
|
+
if isinstance(parent_segments, str):
|
|
21
|
+
parent_segments = [parent_segments]
|
|
22
|
+
self._parent_segments = parent_segments
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def segment(self) -> str:
|
|
26
|
+
return self._segment
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def http(self) -> UptimerHttpLib:
|
|
30
|
+
return self._http
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def url(self) -> str:
|
|
34
|
+
return self._http.build_url(self.path)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def path(self) -> str:
|
|
38
|
+
return "/".join([*self._parent_segments, self._segment])
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
6
|
+
from uptimer.models import from_api_region
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from uptimer.http import UptimerHttpLib
|
|
10
|
+
from uptimer.models.region import Region
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RegionsEndpoint(BaseEndpoint):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
http: UptimerHttpLib,
|
|
17
|
+
parent_segments: str | list[str] | None = None,
|
|
18
|
+
):
|
|
19
|
+
super().__init__(http, "regions", parent_segments)
|
|
20
|
+
|
|
21
|
+
def all(self) -> list[Region]:
|
|
22
|
+
response = self.http.client.get(self.url)
|
|
23
|
+
result = self.http.parse_response(response=response)
|
|
24
|
+
return [from_api_region(region) for region in result]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
7
|
+
from uptimer.models import from_api_rule
|
|
8
|
+
from uptimer.models.rule import DeleteRuleResponse
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from uptimer.http import UptimerHttpLib
|
|
12
|
+
from uptimer.models.rule import (
|
|
13
|
+
BaseRule,
|
|
14
|
+
CreateRuleRequest,
|
|
15
|
+
Rule,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RulesEndpoint(BaseEndpoint):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
http: UptimerHttpLib,
|
|
23
|
+
parent_segments: str | list[str] | None = None,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(http, "rules", parent_segments)
|
|
26
|
+
|
|
27
|
+
def all(self, workspace_id: str) -> list[Rule]:
|
|
28
|
+
"""Get all rules for a specific workspace."""
|
|
29
|
+
params = {"workspace_id": workspace_id}
|
|
30
|
+
response = self.http.client.get(self.url, params=params)
|
|
31
|
+
result = self.http.parse_response(response=response)
|
|
32
|
+
|
|
33
|
+
return [from_api_rule(rule_data) for rule_data in result]
|
|
34
|
+
|
|
35
|
+
def get(self, rule_id: str) -> Rule:
|
|
36
|
+
"""Get a single rule by ID."""
|
|
37
|
+
response = self.http.client.get(f"{self.url}/{rule_id}")
|
|
38
|
+
result = self.http.parse_response(response=response)
|
|
39
|
+
|
|
40
|
+
return from_api_rule(result)
|
|
41
|
+
|
|
42
|
+
def create(self, rule_data: CreateRuleRequest) -> Rule:
|
|
43
|
+
"""Create a new rule."""
|
|
44
|
+
# Use serialization to convert dataclass to dict
|
|
45
|
+
payload = asdict(rule_data)
|
|
46
|
+
|
|
47
|
+
response = self.http.client.post(self.url, json=payload)
|
|
48
|
+
result = self.http.parse_response(response=response)
|
|
49
|
+
|
|
50
|
+
return from_api_rule(result)
|
|
51
|
+
|
|
52
|
+
def update(self, rule_id: str, rule_data: BaseRule) -> Rule:
|
|
53
|
+
"""Update an existing rule."""
|
|
54
|
+
# Use serialization to convert dataclass to dict
|
|
55
|
+
payload = asdict(rule_data)
|
|
56
|
+
|
|
57
|
+
# Remove id field if present (it's already in the URL)
|
|
58
|
+
payload.pop("id", None)
|
|
59
|
+
|
|
60
|
+
response = self.http.client.post(f"{self.url}/{rule_id}", json=payload)
|
|
61
|
+
result = self.http.parse_response(response=response)
|
|
62
|
+
|
|
63
|
+
return from_api_rule(result)
|
|
64
|
+
|
|
65
|
+
def delete(self, rule_id: str) -> DeleteRuleResponse:
|
|
66
|
+
"""Delete a rule by ID."""
|
|
67
|
+
response = self.http.client.delete(f"{self.url}/{rule_id}")
|
|
68
|
+
|
|
69
|
+
# parse_response already returns the result field
|
|
70
|
+
result_data = self.http.parse_response(response=response)
|
|
71
|
+
|
|
72
|
+
return DeleteRuleResponse(
|
|
73
|
+
message=result_data["message"],
|
|
74
|
+
rule_id=result_data["rule_id"],
|
|
75
|
+
)
|
uptimer/endpoints/v1.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
2
|
+
from uptimer.endpoints.regions import RegionsEndpoint
|
|
3
|
+
from uptimer.endpoints.rules import RulesEndpoint
|
|
4
|
+
from uptimer.endpoints.workspaces import WorkspacesEndpoint
|
|
5
|
+
from uptimer.http import UptimerHttpLib
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class V1Endpoint(BaseEndpoint):
|
|
9
|
+
workspaces: WorkspacesEndpoint
|
|
10
|
+
regions: RegionsEndpoint
|
|
11
|
+
rules: RulesEndpoint
|
|
12
|
+
|
|
13
|
+
def __init__(self, http: UptimerHttpLib):
|
|
14
|
+
super().__init__(http, "v1")
|
|
15
|
+
self.workspaces = WorkspacesEndpoint(http, ["v1"])
|
|
16
|
+
self.regions = RegionsEndpoint(http, ["v1"])
|
|
17
|
+
self.rules = RulesEndpoint(http, ["v1"])
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
6
|
+
from uptimer.models import from_api_workspace
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from uptimer.http import UptimerHttpLib
|
|
10
|
+
from uptimer.models.workspace import Workspace
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkspacesEndpoint(BaseEndpoint):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
http: UptimerHttpLib,
|
|
17
|
+
parent_segments: str | list[str] | None = None,
|
|
18
|
+
):
|
|
19
|
+
super().__init__(http, "workspaces", parent_segments)
|
|
20
|
+
|
|
21
|
+
def all(self) -> list[Workspace]:
|
|
22
|
+
response = self.http.client.get(self.url)
|
|
23
|
+
result = self.http.parse_response(response=response)
|
|
24
|
+
return [from_api_workspace(workspace) for workspace in result]
|
uptimer/errors.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UptimerError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UptimerInvalidResponseError(UptimerError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UptimerInvalidHttpCodeError(UptimerError):
|
|
13
|
+
def __init__(self, url: httpx.URL, status_code: int):
|
|
14
|
+
self.url = url
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
super().__init__(f"Invalid HTTP code {status_code!s} for URL {url!s}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DefaultUptimerApiError(UptimerError):
|
|
20
|
+
def __init__(self, error: dict):
|
|
21
|
+
self.code = error.get("code")
|
|
22
|
+
self.error_type = error.get("error_type")
|
|
23
|
+
self.message = error.get("message", "")
|
|
24
|
+
self.details = error.get("details", "")
|
|
25
|
+
super().__init__(f"API error: {self.code} {self.message}")
|
uptimer/http.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from uptimer.endpoints.endpoint import BaseEndpoint
|
|
8
|
+
from uptimer.errors import DefaultUptimerApiError, UptimerInvalidHttpCodeError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WorkspacesEndpoint(BaseEndpoint):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UptimerHttpLib:
|
|
16
|
+
def __init__(self, api_key: str, base_url: str | None = None):
|
|
17
|
+
if base_url is None:
|
|
18
|
+
base_url = "https://api.uptimer.com"
|
|
19
|
+
|
|
20
|
+
self._base_url = base_url.rstrip("/")
|
|
21
|
+
self._http_client = httpx.Client(
|
|
22
|
+
base_url=base_url,
|
|
23
|
+
headers={
|
|
24
|
+
"Authorization": f"Bearer {api_key}",
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def client(self) -> httpx.Client:
|
|
30
|
+
return self._http_client
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def base_url(self) -> str:
|
|
34
|
+
return self._base_url
|
|
35
|
+
|
|
36
|
+
def build_url(self, path: str) -> str:
|
|
37
|
+
path = path.strip("/")
|
|
38
|
+
return self._base_url.rstrip("/") + "/" + path
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def parse_response(
|
|
42
|
+
response: httpx.Response,
|
|
43
|
+
) -> Any: # noqa: ANN401
|
|
44
|
+
if response.status_code != 200:
|
|
45
|
+
raise UptimerInvalidHttpCodeError(
|
|
46
|
+
response.request.url,
|
|
47
|
+
response.status_code,
|
|
48
|
+
)
|
|
49
|
+
data = response.json()
|
|
50
|
+
if data.get("error"):
|
|
51
|
+
raise DefaultUptimerApiError(data["error"])
|
|
52
|
+
return data["result"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .deserialize import (
|
|
2
|
+
from_api,
|
|
3
|
+
from_api_region,
|
|
4
|
+
from_api_rule,
|
|
5
|
+
from_api_workspace,
|
|
6
|
+
)
|
|
7
|
+
from .errors import (
|
|
8
|
+
DeserializationError,
|
|
9
|
+
InvalidDataTypeError,
|
|
10
|
+
MissingKindError,
|
|
11
|
+
ModelError,
|
|
12
|
+
TypeMismatchError,
|
|
13
|
+
UnknownKindError,
|
|
14
|
+
)
|
|
15
|
+
from .region import Region
|
|
16
|
+
from .rule import (
|
|
17
|
+
BaseRule,
|
|
18
|
+
CreateRuleRequest,
|
|
19
|
+
DeleteRuleResponse,
|
|
20
|
+
Rule,
|
|
21
|
+
RuleRequest,
|
|
22
|
+
RuleResponse,
|
|
23
|
+
RuleResponseBody,
|
|
24
|
+
)
|
|
25
|
+
from .workspace import Workspace
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BaseRule",
|
|
29
|
+
"CreateRuleRequest",
|
|
30
|
+
"DeleteRuleResponse",
|
|
31
|
+
"DeserializationError",
|
|
32
|
+
"InvalidDataTypeError",
|
|
33
|
+
"MissingKindError",
|
|
34
|
+
"ModelError",
|
|
35
|
+
"Region",
|
|
36
|
+
"Rule",
|
|
37
|
+
"RuleRequest",
|
|
38
|
+
"RuleResponse",
|
|
39
|
+
"RuleResponseBody",
|
|
40
|
+
"TypeMismatchError",
|
|
41
|
+
"UnknownKindError",
|
|
42
|
+
"Workspace",
|
|
43
|
+
"from_api",
|
|
44
|
+
"from_api_region",
|
|
45
|
+
"from_api_rule",
|
|
46
|
+
"from_api_workspace",
|
|
47
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, TypeVar, Union
|
|
4
|
+
|
|
5
|
+
from .errors import (
|
|
6
|
+
InvalidDataTypeError,
|
|
7
|
+
MissingKindError,
|
|
8
|
+
TypeMismatchError,
|
|
9
|
+
UnknownKindError,
|
|
10
|
+
)
|
|
11
|
+
from .region import Region
|
|
12
|
+
from .rule import (
|
|
13
|
+
Rule,
|
|
14
|
+
RuleRequest,
|
|
15
|
+
RuleResponse,
|
|
16
|
+
RuleResponseBody,
|
|
17
|
+
)
|
|
18
|
+
from .workspace import Workspace
|
|
19
|
+
|
|
20
|
+
# Type variable for the return type
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
# Union type for all possible return types
|
|
24
|
+
DeserializableType = Union[
|
|
25
|
+
Rule,
|
|
26
|
+
RuleRequest,
|
|
27
|
+
RuleResponse,
|
|
28
|
+
RuleResponseBody,
|
|
29
|
+
Region,
|
|
30
|
+
Workspace,
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Type for items that might be deserializable
|
|
34
|
+
DeserializableItem = Union[dict[str, Any], list[Any], Any]
|
|
35
|
+
|
|
36
|
+
# Registry of classes by their kind
|
|
37
|
+
_KIND_REGISTRY = {
|
|
38
|
+
"rule": Rule,
|
|
39
|
+
"rule_request": RuleRequest,
|
|
40
|
+
"rule_response": RuleResponse,
|
|
41
|
+
"rule_response_body": RuleResponseBody,
|
|
42
|
+
"region": Region,
|
|
43
|
+
"workspace": Workspace,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def from_api(data: dict[str, Any]) -> DeserializableType:
|
|
48
|
+
"""
|
|
49
|
+
Universal deserializer that creates objects based on their 'kind' property.
|
|
50
|
+
|
|
51
|
+
Recursively deserializes nested objects.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data: Dictionary containing the object data with a 'kind' property
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The appropriate object instance based on the 'kind' property
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
InvalidDataTypeError: If data is not a dictionary
|
|
61
|
+
MissingKindError: If the 'kind' property is missing
|
|
62
|
+
UnknownKindError: If the 'kind' property is not recognized
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(data, dict):
|
|
65
|
+
raise InvalidDataTypeError(type(data))
|
|
66
|
+
|
|
67
|
+
kind = data.get("kind")
|
|
68
|
+
if not kind:
|
|
69
|
+
raise MissingKindError(data)
|
|
70
|
+
|
|
71
|
+
if kind not in _KIND_REGISTRY:
|
|
72
|
+
raise UnknownKindError(kind)
|
|
73
|
+
|
|
74
|
+
cls = _KIND_REGISTRY[kind]
|
|
75
|
+
|
|
76
|
+
# Create a copy to avoid modifying the original
|
|
77
|
+
obj_data = data.copy()
|
|
78
|
+
|
|
79
|
+
# Recursively deserialize nested objects that have 'kind' properties
|
|
80
|
+
for key, value in obj_data.items():
|
|
81
|
+
if isinstance(value, dict) and value.get("kind"):
|
|
82
|
+
obj_data[key] = from_api(value)
|
|
83
|
+
elif isinstance(value, list):
|
|
84
|
+
# Handle lists of objects
|
|
85
|
+
obj_data[key] = [_deserialize_if_possible(item) for item in value]
|
|
86
|
+
|
|
87
|
+
return cls(**obj_data)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _deserialize_if_possible(item: DeserializableItem) -> DeserializableItem:
|
|
91
|
+
"""Deserialize an item if it has a kind property."""
|
|
92
|
+
if isinstance(item, dict) and item.get("kind"):
|
|
93
|
+
return from_api(item)
|
|
94
|
+
return item
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def from_api_rule(data: dict[str, Any]) -> Rule:
|
|
98
|
+
"""Type-safe deserializer for Rule objects."""
|
|
99
|
+
result = from_api(data)
|
|
100
|
+
if not isinstance(result, Rule):
|
|
101
|
+
msg = "Rule"
|
|
102
|
+
raise TypeMismatchError(msg, type(result).__name__)
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def from_api_region(data: dict[str, Any]) -> Region:
|
|
107
|
+
"""Type-safe deserializer for Region objects."""
|
|
108
|
+
result = from_api(data)
|
|
109
|
+
if not isinstance(result, Region):
|
|
110
|
+
msg = "Region"
|
|
111
|
+
raise TypeMismatchError(msg, type(result).__name__)
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def from_api_workspace(data: dict[str, Any]) -> Workspace:
|
|
116
|
+
"""Type-safe deserializer for Workspace objects."""
|
|
117
|
+
result = from_api(data)
|
|
118
|
+
if not isinstance(result, Workspace):
|
|
119
|
+
msg = "Workspace"
|
|
120
|
+
raise TypeMismatchError(msg, type(result).__name__)
|
|
121
|
+
return result
|
uptimer/models/errors.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Error classes for model operations."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ModelError(Exception):
|
|
5
|
+
"""Base class for all model-related errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeserializationError(ModelError):
|
|
9
|
+
"""Base class for deserialization errors."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MissingKindError(DeserializationError):
|
|
13
|
+
"""Raised when an object is missing the 'kind' property."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, data: dict):
|
|
16
|
+
self.data = data
|
|
17
|
+
super().__init__("Data must contain a 'kind' property")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UnknownKindError(DeserializationError):
|
|
21
|
+
"""Raised when an object has an unknown 'kind' property."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, kind: str):
|
|
24
|
+
self.kind = kind
|
|
25
|
+
super().__init__(f"Unknown kind: {kind}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InvalidDataTypeError(DeserializationError):
|
|
29
|
+
"""Raised when data is not a dictionary."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, data_type: type):
|
|
32
|
+
self.data_type = data_type
|
|
33
|
+
super().__init__(
|
|
34
|
+
f"Data must be a dictionary, got {data_type.__name__}",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TypeMismatchError(DeserializationError):
|
|
39
|
+
"""Raised when the deserialized object is not of the expected type."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, expected_type: str, actual_type: str):
|
|
42
|
+
self.expected_type = expected_type
|
|
43
|
+
self.actual_type = actual_type
|
|
44
|
+
super().__init__(f"Expected {expected_type}, got {actual_type}")
|
uptimer/models/region.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class Region:
|
|
6
|
+
id: str # region id, uuids used for api ids
|
|
7
|
+
name: str # region name
|
|
8
|
+
active_workers_count: int # number of active workers in the region
|
|
9
|
+
kind: str # any object has kind property, defines class of object
|
uptimer/models/rule.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class RuleRequest:
|
|
8
|
+
url: str # request URL
|
|
9
|
+
method: str # HTTP method (GET, POST, etc.)
|
|
10
|
+
content_type: str # content type for the request
|
|
11
|
+
data: str # request data/payload
|
|
12
|
+
kind: str = "rule_request"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RuleResponseBody:
|
|
17
|
+
content: str # expected response body content
|
|
18
|
+
kind: str = "rule_response_body"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class RuleResponse:
|
|
23
|
+
statuses: list[int] # list of acceptable HTTP status codes
|
|
24
|
+
body: RuleResponseBody # expected response body
|
|
25
|
+
kind: str = "rule_response"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class BaseRule:
|
|
30
|
+
"""Base class containing common rule fields."""
|
|
31
|
+
|
|
32
|
+
name: str # rule name
|
|
33
|
+
interval: int # check interval in seconds
|
|
34
|
+
workspace_id: str # workspace id
|
|
35
|
+
request: RuleRequest # request configuration
|
|
36
|
+
response: RuleResponse # response validation
|
|
37
|
+
kind: str = "rule"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Rule(BaseRule):
|
|
42
|
+
"""Complete rule with ID for API responses."""
|
|
43
|
+
|
|
44
|
+
id: str = "" # rule id, uuids used for api ids
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CreateRuleRequest(BaseRule):
|
|
49
|
+
"""Rule data for creation requests (no ID needed)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class DeleteRuleResponse:
|
|
54
|
+
"""Response object for successful rule deletion."""
|
|
55
|
+
|
|
56
|
+
message: str # success message
|
|
57
|
+
rule_id: str # ID of the deleted rule
|
uptimer/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uptimer-python-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Python SDK for uptimer
|
|
5
|
+
Project-URL: Repository, https://github.com/myuptime-info/uptimer-python-sdk
|
|
6
|
+
Author-email: Roman Zadoev <zadoev@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: NOTICE
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: httpx>=0.28.1
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Uptimer Python SDK
|
|
15
|
+
|
|
16
|
+
A Python SDK for [uptimer](https://uptimer.myuptime.info/) - a monitoring and uptime checking service.
|
|
17
|
+
|
|
18
|
+
## License
|
|
19
|
+
|
|
20
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
21
|
+
|
|
22
|
+
For third-party license information, see the [NOTICE](NOTICE) file.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from uptimer.client import UptimerClient
|
|
28
|
+
from uptimer.models.rule import CreateRuleRequest, RuleRequest, RuleResponse, RuleResponseBody
|
|
29
|
+
from uptimer.errors import DefaultUptimerApiError, UptimerInvalidHttpCodeError, UptimerError
|
|
30
|
+
# Initialize the client
|
|
31
|
+
client = UptimerClient(
|
|
32
|
+
api_key="your-api-key-here",
|
|
33
|
+
base_url="http://127.0.0.1:2517/api", # or your custom base URL
|
|
34
|
+
)
|
|
35
|
+
regions = client.v1.regions.all()
|
|
36
|
+
workspaces = client.v1.workspaces.all()
|
|
37
|
+
workspace_id = workspaces[0].id
|
|
38
|
+
rules =client.v1.rules.all(workspace_id)
|
|
39
|
+
|
|
40
|
+
new_rule = client.v1.rules.create(
|
|
41
|
+
CreateRuleRequest(
|
|
42
|
+
name="My Test Rule",
|
|
43
|
+
interval=60, # Check every 60 seconds
|
|
44
|
+
workspace_id=workspace_id,
|
|
45
|
+
request=RuleRequest(
|
|
46
|
+
url="https://example.com",
|
|
47
|
+
method="GET", # PATCH, POST, HEAD
|
|
48
|
+
content_type="application/json", # expected content type
|
|
49
|
+
data="", # data (substring) that should be contained in resonse
|
|
50
|
+
),
|
|
51
|
+
response=RuleResponse(
|
|
52
|
+
statuses=[200, 201, 202], # any of this status means site is up
|
|
53
|
+
body=RuleResponseBody(content="expected response"),
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
new_rule_updated = client.v1.rules.update(
|
|
59
|
+
new_rule.id,
|
|
60
|
+
CreateRuleRequest(
|
|
61
|
+
name="Updated Rule Name",
|
|
62
|
+
interval=120, # Change to 2 minutes
|
|
63
|
+
workspace_id=workspace_id,
|
|
64
|
+
request=RuleRequest(
|
|
65
|
+
url="https://updated-example.com",
|
|
66
|
+
method="POST",
|
|
67
|
+
content_type="application/json",
|
|
68
|
+
data='{"key": "value"}',
|
|
69
|
+
),
|
|
70
|
+
response=RuleResponse(
|
|
71
|
+
statuses=[200, 201],
|
|
72
|
+
body=RuleResponseBody(content="updated expected response"),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# caching errors on delete example
|
|
78
|
+
try:
|
|
79
|
+
client.v1.rules.delete(new_rule_updated.id)
|
|
80
|
+
except DefaultUptimerApiError as e:
|
|
81
|
+
# error responses from uptimer server
|
|
82
|
+
print(
|
|
83
|
+
e.message, # user message
|
|
84
|
+
e.code, # error id
|
|
85
|
+
e.error_type, # class of error,
|
|
86
|
+
e.details, # detailed message for a developer
|
|
87
|
+
)
|
|
88
|
+
except UptimerInvalidHttpCodeError as e:
|
|
89
|
+
# uptimer api always return 200, if not -> http transport error
|
|
90
|
+
# for an example 404 status is really page (url) not found, it doesn't mean that an object with id not found.
|
|
91
|
+
print(
|
|
92
|
+
e.url,
|
|
93
|
+
e.status_code,
|
|
94
|
+
)
|
|
95
|
+
except UptimerError as e: # base error, if you need one
|
|
96
|
+
raise
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Also, check out [examples directory](https://github.com/myuptime-info/uptimer-python-sdk/examples)
|
|
100
|
+
|
|
101
|
+
### Development Setup
|
|
102
|
+
|
|
103
|
+
1. Clone the repository:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone <repository-url>
|
|
107
|
+
cd uptimer-python-sdk
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
2. Install dependencies:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
uv sync --dev
|
|
114
|
+
# for integration tests
|
|
115
|
+
uv run playwright install chromium
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
3. Run tests:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
uv run pytest
|
|
122
|
+
# integration
|
|
123
|
+
docker pull myuptime/uptimer
|
|
124
|
+
docker run -p 2517:2517 myuptime/uptimer
|
|
125
|
+
UPTIMER_URL=http://localhost:2517 uv run --integration
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
4. Run linting:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv run ruff check .
|
|
132
|
+
uv run mypy src
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
5. Format code:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
uv run ruff format .
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
6. Run pre-commit hooks:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv run pre-commit run --all-files
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Third-Party Licenses
|
|
148
|
+
|
|
149
|
+
This project uses the following third-party libraries:
|
|
150
|
+
|
|
151
|
+
### Production Dependencies
|
|
152
|
+
|
|
153
|
+
- **httpx** (BSD 3-Clause License) - HTTP client for Python
|
|
154
|
+
|
|
155
|
+
### Development Dependencies
|
|
156
|
+
|
|
157
|
+
- **mypy** (Apache 2.0 License) - Static type checker
|
|
158
|
+
- **playwright** (Apache 2.0 License) - Browser automation
|
|
159
|
+
- **pre-commit** (MIT License) - Git hooks framework
|
|
160
|
+
- **pytest** (MIT License) - Testing framework
|
|
161
|
+
- **pytest-cov** (MIT License) - Coverage plugin for pytest
|
|
162
|
+
- **pytest-httpx** (MIT License) - HTTPX plugin for pytest
|
|
163
|
+
- **pytest-playwright** (MIT License) - Playwright plugin for pytest
|
|
164
|
+
- **responses** (Apache 2.0 License) - Mock library for requests
|
|
165
|
+
- **ruff** (MIT License) - Fast Python linter and formatter
|
|
166
|
+
|
|
167
|
+
All third-party licenses are compatible with the MIT License used by this project. Note that the BSD 3-Clause License (used by httpx) includes an additional restriction prohibiting the use of the copyright holder's name for endorsement without permission.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
uptimer/__init__.py,sha256=xPKvxeYbWVTJxqdgCWKJV6_t24PlvBGs0qn6SiuAGAk,49
|
|
2
|
+
uptimer/client.py,sha256=VCjXlg4xvbhBVD3iA_rOU1jr_qTcKYSdd_ZmA9yorUQ,664
|
|
3
|
+
uptimer/errors.py,sha256=ttei31pQhKnozY4xbDh3oMQEh3s8tNj4z6ed0_Htwkk,699
|
|
4
|
+
uptimer/http.py,sha256=fKFZzsol6SuD1RvgRKds7VqKksbK-KEJN7Yqt5i_02E,1369
|
|
5
|
+
uptimer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
uptimer/endpoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
uptimer/endpoints/endpoint.py,sha256=TLITtDy4C-Mqs7dWnL3TYwyaoR2HnjqnCT_rTqI7T-s,917
|
|
8
|
+
uptimer/endpoints/regions.py,sha256=pJOT2VuUTCDnSpsuQNLvVGcjTWbKeKc0AMRkXGZxv5s,705
|
|
9
|
+
uptimer/endpoints/rules.py,sha256=7iwpbR-XPFtlWLRX1QYy4xVKDeNbAzG5PCf2boFOoEY,2470
|
|
10
|
+
uptimer/endpoints/v1.py,sha256=Hbl8AUrR4Pn_4a685V1-f_mv9ZI_BWU7fp-qBhp6W_w,624
|
|
11
|
+
uptimer/endpoints/workspaces.py,sha256=FNMn7kGQ8uy6ecMjVrzOOMDuAS6UmMz3wUkL3IpIl34,732
|
|
12
|
+
uptimer/models/__init__.py,sha256=3BIJH0JzLbGJumJTgdEAQYN09JW9UXRMK0XSsEp78Bk,897
|
|
13
|
+
uptimer/models/deserialize.py,sha256=fprfBiF9BK6ccGS8p4TZMBmUTezRZG-tfdv1SAe4SwE,3294
|
|
14
|
+
uptimer/models/errors.py,sha256=xuKwRpX-MQjWggJYsqaM4p0bcvRLDKuWbCmH0JdpOQs,1299
|
|
15
|
+
uptimer/models/region.py,sha256=PEZpXXDaoF884dfiIYfHBh1Akmr0fhjODtqVcXDzQYM,282
|
|
16
|
+
uptimer/models/rule.py,sha256=ZzLpPBeOH0Qtl-bw9Jk_z_VvKnsUu6EEDKgvLZ-mCR4,1346
|
|
17
|
+
uptimer/models/workspace.py,sha256=zxD05PuNcnGBBqB42X_qRGG_oqxDuW-iTafereiw5Q4,259
|
|
18
|
+
uptimer_python_sdk-0.2.0.dist-info/METADATA,sha256=S1_QtLhnEW3yR4K3dhT_2fqghliicPM_SXS-qN0Grtg,4732
|
|
19
|
+
uptimer_python_sdk-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
20
|
+
uptimer_python_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=f5hNPk9nMiFFcs8hvC1uy5erybWlFm-7iHCOJd4HATc,1069
|
|
21
|
+
uptimer_python_sdk-0.2.0.dist-info/licenses/NOTICE,sha256=gimzxqrDPQ8cq3bH6aXBkuoPek6DbQwKS6Ix56F1CR4,1628
|
|
22
|
+
uptimer_python_sdk-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Roman Zadoev
|
|
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,18 @@
|
|
|
1
|
+
Uptimer Python SDK
|
|
2
|
+
Copyright (c) 2025 myuptime.info
|
|
3
|
+
|
|
4
|
+
This software includes the following third-party components:
|
|
5
|
+
|
|
6
|
+
httpx
|
|
7
|
+
Copyright © 2019, Encode OSS Ltd (https://www.encode.io/).
|
|
8
|
+
All rights reserved.
|
|
9
|
+
|
|
10
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
11
|
+
|
|
12
|
+
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
13
|
+
|
|
14
|
+
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
- Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|