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 ADDED
@@ -0,0 +1,3 @@
1
+ """Uptimer Python SDK."""
2
+
3
+ __version__ = "0.1.0"
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
+ )
@@ -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
@@ -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}")
@@ -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
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Workspace:
6
+ id: str # workspace id, uuids used for api ids
7
+ name: str # workspace name
8
+ role: str # user role in workspace
9
+ kind: str # any object has kind property, defines class of object
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.