uptimer-python-sdk 0.4.0__py3-none-any.whl → 1.6.0rc0__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 CHANGED
@@ -1,3 +1,12 @@
1
- """Uptimer Python SDK."""
1
+ """
2
+ Uptimer Python SDK.
2
3
 
3
- __version__ = "0.4.0"
4
+ Targets Uptimer API v2 only. Code written against 0.4.x keeps working against
5
+ the server — API v1 is unchanged and supported — but must stay on the 0.4.x SDK.
6
+
7
+ The version tracks the uptimer release this SDK targets: 1.6.x speaks to
8
+ uptimer 1.6.0 and later. Patch numbers are independent, so an SDK fix can ship
9
+ without a server release. See product Decision 0013.
10
+ """
11
+
12
+ __version__ = "1.6.0-rc.0"
uptimer/client.py CHANGED
@@ -2,25 +2,70 @@ from __future__ import annotations
2
2
 
3
3
  from typing import cast
4
4
 
5
- from uptimer.endpoints.v1 import V1Endpoint
5
+ from uptimer.compat import ensure_v2_supported
6
+ from uptimer.endpoints.v2 import V2Endpoint
6
7
  from uptimer.http import UptimerHttpLib
7
8
 
8
9
 
9
10
  class UptimerClient:
10
- v1: V1Endpoint
11
+ """
12
+ The Uptimer API client.
13
+
14
+ Resources are reached through the API version that serves them:
15
+ `client.v2.workspaces`, `client.v2.locations`, `client.v2.incidents`,
16
+ `client.v2.monitoring.websites` and
17
+ `client.v2.subjects(subject).signals(signal).observations`. This SDK does
18
+ not speak API v1 — see the migration note in the README if you are coming
19
+ from 0.4.x.
20
+
21
+ `version()` and the compatibility helpers stay here rather than under a
22
+ version namespace, because `/version` is shared and unversioned.
23
+ """
24
+
25
+ v2: V2Endpoint
11
26
 
12
27
  def __init__(self, api_key: str, base_url: str):
13
28
  self._http_lib = UptimerHttpLib(api_key, base_url)
14
- self.v1 = V1Endpoint(self._http_lib)
29
+ self._checked_compat = False
30
+ self._wire()
31
+
32
+ def _wire(self) -> None:
33
+ self.v2 = V2Endpoint(self._http_lib)
15
34
 
16
35
  def version(self) -> str:
36
+ """
37
+ Return the server version.
38
+
39
+ `/version` is a shared global endpoint, not a versioned one, so this
40
+ works against any server — including one too old for the rest of this
41
+ SDK.
42
+ """
17
43
  response = self._http_lib.client.get(self._http_lib.build_url("version"))
18
44
  return cast("str", self._http_lib.parse_response(response=response))
19
45
 
46
+ def check_compatibility(self) -> str:
47
+ """
48
+ Verify the server provides API v2, and return its version.
49
+
50
+ Raises IncompatibleServerError if it does not. Called once per client
51
+ by ensure_compatible(); call it directly to fail fast at startup.
52
+ """
53
+ version = self.version()
54
+ ensure_v2_supported(version)
55
+ self._checked_compat = True
56
+ return version
57
+
58
+ def ensure_compatible(self) -> None:
59
+ """Run the compatibility check once, then never again."""
60
+ if not self._checked_compat:
61
+ self.check_compatibility()
62
+
20
63
  def set_uptimer_http_lib(self, http_lib: UptimerHttpLib) -> None:
21
64
  self._http_lib = http_lib
65
+ self._checked_compat = False
66
+ self._wire()
22
67
 
23
68
 
24
69
  class UptimerCloudClient(UptimerClient):
25
70
  def __init__(self, api_key: str):
26
- super().__init__(api_key, "https://api.myuptime.info")
71
+ super().__init__(api_key, "https://myuptime.info/api")
uptimer/compat.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ Server compatibility.
3
+
4
+ This SDK targets API v2 only. A server that predates v2 answers /v2 with 404,
5
+ which on its own tells a user nothing — so the version is checked first and the
6
+ 404 is translated as a fallback.
7
+
8
+ Why both: `uptimer` and `myuptime.info` version independently over the same
9
+ shared API (1.5.x and 15.x). A numeric minimum catches the self-hosted case
10
+ cleanly, but cannot express "hosted 14.0.3 has no v2" — 14 is greater than 1.
11
+ The 404 path catches what the number cannot.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ from uptimer import __version__
19
+ from uptimer.errors import IncompatibleServerError
20
+
21
+
22
+ def _minimum_from_own_version() -> tuple[int, int, int]:
23
+ """
24
+ Return the oldest server version this SDK speaks to.
25
+
26
+ The SDK's major.minor tracks the uptimer release it targets (Decision
27
+ 0013), so 1.5.x requires a 1.5.0 server. Deriving it here rather than
28
+ writing the number down twice means the two cannot drift apart.
29
+ """
30
+ parts = __version__.split(".")
31
+ return (int(parts[0]), int(parts[1]), 0)
32
+
33
+
34
+ MINIMUM_UPTIMER_VERSION = _minimum_from_own_version()
35
+
36
+ _VERSION_RE = re.compile(r"^v?(\d+)\.(\d+)\.(\d+)")
37
+
38
+
39
+ def parse_version(version: str) -> tuple[int, int, int] | None:
40
+ """
41
+ Parse a server version, or None if it is not a release number.
42
+
43
+ "dev" and anything unparseable return None and are treated as usable: a
44
+ developer running from source must not be locked out by a version string.
45
+ """
46
+ match = _VERSION_RE.match(version.strip())
47
+ if not match:
48
+ return None
49
+ return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
50
+
51
+
52
+ def ensure_v2_supported(version: str) -> None:
53
+ """Raise IncompatibleServerError if this server is too old for API v2."""
54
+ parsed = parse_version(version)
55
+ if parsed is None:
56
+ return
57
+ if parsed < MINIMUM_UPTIMER_VERSION:
58
+ raise IncompatibleServerError(version)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from uptimer.endpoints.endpoint import BaseEndpoint
6
+ from uptimer.models.v2 import from_api_incident
7
+
8
+ if TYPE_CHECKING:
9
+ from uptimer.http import UptimerHttpLib
10
+ from uptimer.models.v2 import Incident
11
+
12
+
13
+ class IncidentsEndpoint(BaseEndpoint):
14
+ def __init__(
15
+ self,
16
+ http: UptimerHttpLib,
17
+ parent_segments: str | list[str] | None = None,
18
+ ):
19
+ super().__init__(http, "incidents", parent_segments)
20
+
21
+ def all(
22
+ self,
23
+ workspace_id: str,
24
+ monitor_id: str | None = None,
25
+ ) -> list[Incident]:
26
+ """
27
+ Open incidents in a workspace, newest trouble first.
28
+
29
+ Only open ones: this answers "what is wrong now". Pass monitor_id to
30
+ narrow it to a single monitor.
31
+ """
32
+ params = {"workspace_id": workspace_id}
33
+ if monitor_id:
34
+ params["monitor_id"] = monitor_id
35
+ response = self.http.client.get(self.url, params=params)
36
+ result = self.http.parse_response(response=response)
37
+ return [from_api_incident(item) for item in result]
@@ -3,22 +3,23 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from uptimer.endpoints.endpoint import BaseEndpoint
6
- from uptimer.models import from_api_region
6
+ from uptimer.models.v2 import from_api_location
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from uptimer.http import UptimerHttpLib
10
- from uptimer.models.region import Region
10
+ from uptimer.models.v2 import Location
11
11
 
12
12
 
13
- class RegionsEndpoint(BaseEndpoint):
13
+ class LocationsEndpoint(BaseEndpoint):
14
14
  def __init__(
15
15
  self,
16
16
  http: UptimerHttpLib,
17
17
  parent_segments: str | list[str] | None = None,
18
18
  ):
19
- super().__init__(http, "regions", parent_segments)
19
+ super().__init__(http, "locations", parent_segments)
20
20
 
21
- def all(self) -> list[Region]:
21
+ def all(self) -> list[Location]:
22
+ """Every location checks can run from."""
22
23
  response = self.http.client.get(self.url)
23
24
  result = self.http.parse_response(response=response)
24
- return [from_api_region(region) for region in result]
25
+ return [from_api_location(item) for item in result]
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ from typing import TYPE_CHECKING
5
+ from urllib.parse import quote
6
+
7
+ from uptimer.endpoints.endpoint import BaseEndpoint
8
+ from uptimer.models.v2 import from_api_observation
9
+
10
+ if TYPE_CHECKING:
11
+ from uptimer.http import UptimerHttpLib
12
+ from uptimer.models.v2 import CreateObservationRequest, Observation
13
+
14
+
15
+ def _slug(value: str, what: str) -> str:
16
+ """
17
+ Escape one slug for a URL path segment.
18
+
19
+ A slug is the API name of a subject or signal, and it lands in the path. It
20
+ is quoted rather than trusted so a value containing a slash addresses a
21
+ signal named that, instead of silently reaching a different resource.
22
+ """
23
+ if not value or not value.strip():
24
+ message = f"{what} slug is required"
25
+ raise ValueError(message)
26
+ return quote(value, safe="")
27
+
28
+
29
+ def _payload(observation: CreateObservationRequest) -> dict:
30
+ """
31
+ Serialize an observation, omitting what was not set.
32
+
33
+ The server rejects unknown fields and distinguishes an absent optional from
34
+ a null one — an absent `observed_at` means "stamp it now", where a null
35
+ would be a malformed timestamp. So unset fields are left out rather than
36
+ sent as None.
37
+ """
38
+ body = asdict(observation)
39
+ return {key: value for key, value in body.items() if value is not None}
40
+
41
+
42
+ class ObservationsEndpoint(BaseEndpoint):
43
+ """
44
+ The observations of one signal.
45
+
46
+ Reached through the signal that owns them —
47
+ `client.v2.subjects(subject).signals(signal).observations` — because a
48
+ signal slug is unique within its subject, not across the workspace. The pair
49
+ is the address.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ http: UptimerHttpLib,
55
+ parent_segments: str | list[str] | None = None,
56
+ ):
57
+ super().__init__(http, "observations", parent_segments)
58
+
59
+ def create(self, observation: CreateObservationRequest) -> Observation:
60
+ """
61
+ Report one observation, and return it as the server stored it.
62
+
63
+ Only custom heartbeat and event signals accept this. A platform HTTP
64
+ signal is written by Uptimer's own probe and refuses posted
65
+ observations, which arrives as a DefaultUptimerApiError.
66
+
67
+ A stored observation the engine will not evaluate is RETURNED, not
68
+ raised: check `accepted` and `reject_reason` on the result. An error is
69
+ raised only when nothing was stored — a bad status, an unparsable
70
+ timestamp, a signal that does not exist, or no permission.
71
+ """
72
+ response = self.http.client.post(self.url, json=_payload(observation))
73
+ result = self.http.parse_response(response=response)
74
+ return from_api_observation(result)
75
+
76
+
77
+ class SignalEndpoint(BaseEndpoint):
78
+ """One signal of a subject, addressed by its slug."""
79
+
80
+ observations: ObservationsEndpoint
81
+
82
+ def __init__(
83
+ self,
84
+ http: UptimerHttpLib,
85
+ signal_slug: str,
86
+ parent_segments: str | list[str] | None = None,
87
+ ):
88
+ super().__init__(http, _slug(signal_slug, "signal"), parent_segments)
89
+ self.observations = ObservationsEndpoint(
90
+ http,
91
+ [*self._parent_segments, self.segment],
92
+ )
93
+
94
+
95
+ class SignalsEndpoint(BaseEndpoint):
96
+ """
97
+ The signals of one subject.
98
+
99
+ Call it with a slug to reach one:
100
+ `client.v2.subjects("checkout").signals("worker-pulse")`.
101
+
102
+ There is no listing or authoring here. Signals are created and managed in
103
+ the Uptimer UI; the SDK exists to report data to one that already exists.
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ http: UptimerHttpLib,
109
+ parent_segments: str | list[str] | None = None,
110
+ ):
111
+ super().__init__(http, "signals", parent_segments)
112
+
113
+ def __call__(self, signal_slug: str) -> SignalEndpoint:
114
+ return SignalEndpoint(self.http, signal_slug, self._parent_segments_with_self())
115
+
116
+ def _parent_segments_with_self(self) -> list[str]:
117
+ return [*self._parent_segments, self.segment]
118
+
119
+
120
+ class SubjectEndpoint(BaseEndpoint):
121
+ """One monitored subject, addressed by its slug."""
122
+
123
+ signals: SignalsEndpoint
124
+
125
+ def __init__(
126
+ self,
127
+ http: UptimerHttpLib,
128
+ subject_slug: str,
129
+ parent_segments: str | list[str] | None = None,
130
+ ):
131
+ super().__init__(http, _slug(subject_slug, "subject"), parent_segments)
132
+ self.signals = SignalsEndpoint(http, [*self._parent_segments, self.segment])
133
+
134
+
135
+ class SubjectsEndpoint(BaseEndpoint):
136
+ """
137
+ The monitored subjects.
138
+
139
+ Call it with a slug to reach one: `client.v2.subjects("checkout")`.
140
+
141
+ Deliberately narrow: there is no subject CRUD here. The one thing this
142
+ namespace exists for is reporting observations to a custom signal, and the
143
+ path to that is the subject that owns it.
144
+ """
145
+
146
+ def __init__(
147
+ self,
148
+ http: UptimerHttpLib,
149
+ parent_segments: str | list[str] | None = None,
150
+ ):
151
+ super().__init__(http, "subjects", parent_segments)
152
+
153
+ def __call__(self, subject_slug: str) -> SubjectEndpoint:
154
+ return SubjectEndpoint(self.http, subject_slug, [*self._parent_segments, self.segment])
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from uptimer.endpoints.endpoint import BaseEndpoint
6
+ from uptimer.endpoints.incidents import IncidentsEndpoint
7
+ from uptimer.endpoints.locations import LocationsEndpoint
8
+ from uptimer.endpoints.subjects import SubjectsEndpoint
9
+ from uptimer.endpoints.websites import MonitoringEndpoint
10
+ from uptimer.endpoints.workspaces import WorkspacesEndpoint
11
+
12
+ if TYPE_CHECKING:
13
+ from uptimer.http import UptimerHttpLib
14
+
15
+
16
+ class V2Endpoint(BaseEndpoint):
17
+ """
18
+ API v2, the whole of it: `client.v2`.
19
+
20
+ The API is versioned by path, so the SDK keeps that version visible rather
21
+ than hiding it behind bare attributes (Decision 0012). Every resource here
22
+ builds its URL from this namespace's segment, which is why `/v2` appears in
23
+ exactly one place.
24
+
25
+ `/version` is shared and unversioned, so it stays on the client itself.
26
+ """
27
+
28
+ workspaces: WorkspacesEndpoint
29
+ locations: LocationsEndpoint
30
+ incidents: IncidentsEndpoint
31
+ monitoring: MonitoringEndpoint
32
+ subjects: SubjectsEndpoint
33
+
34
+ def __init__(self, http: UptimerHttpLib):
35
+ super().__init__(http, "v2")
36
+ parent = [self.segment]
37
+ self.workspaces = WorkspacesEndpoint(http, parent)
38
+ self.locations = LocationsEndpoint(http, parent)
39
+ self.incidents = IncidentsEndpoint(http, parent)
40
+ self.monitoring = MonitoringEndpoint(http, parent)
41
+ self.subjects = SubjectsEndpoint(http, parent)
@@ -0,0 +1,116 @@
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.v2 import DeleteWebsiteMonitorResponse, from_api_website_monitor
8
+
9
+ if TYPE_CHECKING:
10
+ from uptimer.http import UptimerHttpLib
11
+ from uptimer.models.v2 import (
12
+ CreateWebsiteMonitorRequest,
13
+ UpdateWebsiteMonitorRequest,
14
+ WebsiteMonitor,
15
+ )
16
+
17
+
18
+ def _strip_kinds(value: object) -> object:
19
+ """
20
+ Drop every `kind` from a request body, at any depth.
21
+
22
+ `kind` is what the server tells a client an object is; it is not something a
23
+ client sets. Echoing it back suggests otherwise, and the server ignores it
24
+ either way.
25
+ """
26
+ if isinstance(value, dict):
27
+ return {k: _strip_kinds(v) for k, v in value.items() if k != "kind"}
28
+ if isinstance(value, list):
29
+ return [_strip_kinds(v) for v in value]
30
+ return value
31
+
32
+
33
+ def _payload(data: object) -> dict:
34
+ """
35
+ Serialize a request dataclass, dropping fields the server fills in.
36
+
37
+ An empty agreement means "leave it alone", which the server expresses by
38
+ omission, so it is stripped rather than sent as "" — those are different
39
+ requests.
40
+ """
41
+ body = asdict(data) # type: ignore[call-overload]
42
+ body.pop("id", None)
43
+ if not body.get("agreement"):
44
+ body.pop("agreement", None)
45
+ return _strip_kinds(body) # type: ignore[return-value]
46
+
47
+
48
+ class WebsitesEndpoint(BaseEndpoint):
49
+ """
50
+ Website monitoring: the built-in template that watches a URL.
51
+
52
+ Nested under /v2/monitoring because it is one template among the several
53
+ coming later, not the general monitor model.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ http: UptimerHttpLib,
59
+ parent_segments: str | list[str] | None = None,
60
+ ):
61
+ super().__init__(http, "websites", parent_segments)
62
+
63
+ def all(self, workspace_id: str) -> list[WebsiteMonitor]:
64
+ """Every website monitor in a workspace."""
65
+ params = {"workspace_id": workspace_id}
66
+ response = self.http.client.get(self.url, params=params)
67
+ result = self.http.parse_response(response=response)
68
+ return [from_api_website_monitor(item) for item in result]
69
+
70
+ def get(self, monitor_id: str) -> WebsiteMonitor:
71
+ """One website monitor by id."""
72
+ response = self.http.client.get(f"{self.url}/{monitor_id}")
73
+ result = self.http.parse_response(response=response)
74
+ return from_api_website_monitor(result)
75
+
76
+ def create(self, monitor: CreateWebsiteMonitorRequest) -> WebsiteMonitor:
77
+ """Create a website monitor, with its signal and rule."""
78
+ response = self.http.client.post(self.url, json=_payload(monitor))
79
+ result = self.http.parse_response(response=response)
80
+ return from_api_website_monitor(result)
81
+
82
+ def update(
83
+ self,
84
+ monitor_id: str,
85
+ monitor: UpdateWebsiteMonitorRequest,
86
+ ) -> WebsiteMonitor:
87
+ """Replace a website monitor's configuration."""
88
+ response = self.http.client.post(
89
+ f"{self.url}/{monitor_id}",
90
+ json=_payload(monitor),
91
+ )
92
+ result = self.http.parse_response(response=response)
93
+ return from_api_website_monitor(result)
94
+
95
+ def delete(self, monitor_id: str) -> DeleteWebsiteMonitorResponse:
96
+ """Delete a website monitor and everything under it."""
97
+ response = self.http.client.delete(f"{self.url}/{monitor_id}")
98
+ result = self.http.parse_response(response=response)
99
+ return DeleteWebsiteMonitorResponse(
100
+ message=result["message"],
101
+ monitor_id=result["monitor_id"],
102
+ )
103
+
104
+
105
+ class MonitoringEndpoint(BaseEndpoint):
106
+ """The monitoring templates. Today there is one: websites."""
107
+
108
+ websites: WebsitesEndpoint
109
+
110
+ def __init__(
111
+ self,
112
+ http: UptimerHttpLib,
113
+ parent_segments: str | list[str] | None = None,
114
+ ):
115
+ super().__init__(http, "monitoring", parent_segments)
116
+ self.websites = WebsitesEndpoint(http, [*self._parent_segments, "monitoring"])
@@ -3,11 +3,11 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from uptimer.endpoints.endpoint import BaseEndpoint
6
- from uptimer.models import from_api_workspace
6
+ from uptimer.models.v2 import from_api_workspace
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from uptimer.http import UptimerHttpLib
10
- from uptimer.models.workspace import Workspace
10
+ from uptimer.models.v2 import Workspace
11
11
 
12
12
 
13
13
  class WorkspacesEndpoint(BaseEndpoint):
@@ -19,6 +19,7 @@ class WorkspacesEndpoint(BaseEndpoint):
19
19
  super().__init__(http, "workspaces", parent_segments)
20
20
 
21
21
  def all(self) -> list[Workspace]:
22
+ """Every workspace this API key can reach."""
22
23
  response = self.http.client.get(self.url)
23
24
  result = self.http.parse_response(response=response)
24
- return [from_api_workspace(workspace) for workspace in result]
25
+ return [from_api_workspace(item) for item in result]
uptimer/errors.py CHANGED
@@ -23,3 +23,21 @@ class DefaultUptimerApiError(UptimerError):
23
23
  self.message = error.get("message", "")
24
24
  self.details = error.get("details", "")
25
25
  super().__init__(f"API error: {self.code} {self.message}")
26
+
27
+
28
+ class IncompatibleServerError(UptimerError):
29
+ """
30
+ The server does not provide API v2, which this SDK requires.
31
+
32
+ Carries the server's own version so the message names the situation rather
33
+ than surfacing a bare 404.
34
+ """
35
+
36
+ def __init__(self, server_version: str):
37
+ self.server_version = server_version
38
+ super().__init__(
39
+ f"This server reports version {server_version}, which does not "
40
+ "provide API v2. uptimer-python-sdk 1.5.x requires API v2 "
41
+ "(uptimer 1.5.0+ or myuptime.info 15.1.0+). For API v1, use "
42
+ "uptimer-python-sdk 0.4.x.",
43
+ )
uptimer/http.py CHANGED
@@ -5,7 +5,11 @@ from typing import Any
5
5
  import httpx
6
6
 
7
7
  from uptimer.endpoints.endpoint import BaseEndpoint
8
- from uptimer.errors import DefaultUptimerApiError, UptimerInvalidHttpCodeError
8
+ from uptimer.errors import (
9
+ DefaultUptimerApiError,
10
+ IncompatibleServerError,
11
+ UptimerInvalidHttpCodeError,
12
+ )
9
13
 
10
14
 
11
15
  class WorkspacesEndpoint(BaseEndpoint):
@@ -38,6 +42,13 @@ class UptimerHttpLib:
38
42
  def parse_response(
39
43
  response: httpx.Response,
40
44
  ) -> Any: # noqa: ANN401
45
+ if response.status_code == 404 and "/v2/" in str(response.request.url):
46
+ # A server without API v2 has no /v2 routes at all. Saying "404"
47
+ # here would leave the user guessing; the version gate in
48
+ # client.check_compatibility() catches this earlier when the
49
+ # version number can express it, and this catches the rest.
50
+ unknown = "unknown (no /v2 routes)"
51
+ raise IncompatibleServerError(unknown)
41
52
  if response.status_code != 200:
42
53
  raise UptimerInvalidHttpCodeError(
43
54
  response.request.url,
@@ -1,9 +1,15 @@
1
- from .deserialize import (
2
- from_api,
3
- from_api_region,
4
- from_api_rule,
5
- from_api_workspace,
6
- )
1
+ """
2
+ Model exceptions, and the versioned model namespaces beneath this one.
3
+
4
+ **No API type is exported here.** v2's types live in `uptimer.models.v2`:
5
+
6
+ from uptimer.models.v2 import CreateWebsiteMonitorRequest, Location
7
+
8
+ Only the deserialization exceptions sit at this level, because they are
9
+ version-independent — the same `TypeMismatchError` is raised whichever API
10
+ version produced the payload.
11
+ """
12
+
7
13
  from .errors import (
8
14
  DeserializationError,
9
15
  InvalidDataTypeError,
@@ -12,36 +18,12 @@ from .errors import (
12
18
  TypeMismatchError,
13
19
  UnknownKindError,
14
20
  )
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
21
 
27
22
  __all__ = [
28
- "BaseRule",
29
- "CreateRuleRequest",
30
- "DeleteRuleResponse",
31
23
  "DeserializationError",
32
24
  "InvalidDataTypeError",
33
25
  "MissingKindError",
34
26
  "ModelError",
35
- "Region",
36
- "Rule",
37
- "RuleRequest",
38
- "RuleResponse",
39
- "RuleResponseBody",
40
27
  "TypeMismatchError",
41
28
  "UnknownKindError",
42
- "Workspace",
43
- "from_api",
44
- "from_api_region",
45
- "from_api_rule",
46
- "from_api_workspace",
47
29
  ]