edgecron-python 1.0.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.
edgecron/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """EdgeCron Python SDK."""
2
+
3
+ from .client import EdgeCron
4
+ from .error import APIError, is_api_error
5
+ from .types import (
6
+ CreateEndpointRequest,
7
+ CreateRetryPolicyRequest,
8
+ CreateScheduleRequest,
9
+ CreateTaskRequest,
10
+ Delivery,
11
+ DeliveryList,
12
+ EndpointList,
13
+ Event,
14
+ EventList,
15
+ PublishEventRequest,
16
+ PublishEventResult,
17
+ ResourceLimits,
18
+ RetryDeliveryResult,
19
+ RetryJob,
20
+ RetryJobList,
21
+ RetryPolicy,
22
+ RetryPolicyList,
23
+ Schedule,
24
+ ScheduleList,
25
+ SubscriptionQuota,
26
+ Task,
27
+ TaskList,
28
+ UpdateEndpointRequest,
29
+ UpdateRetryPolicyRequest,
30
+ UpdateScheduleRequest,
31
+ UsageRecords,
32
+ WebhookEndpoint,
33
+ )
34
+
35
+ __version__ = "1.2.0"
36
+
37
+ __all__ = [
38
+ "APIError",
39
+ "CreateEndpointRequest",
40
+ "CreateRetryPolicyRequest",
41
+ "CreateScheduleRequest",
42
+ "CreateTaskRequest",
43
+ "Delivery",
44
+ "DeliveryList",
45
+ "EdgeCron",
46
+ "EndpointList",
47
+ "Event",
48
+ "EventList",
49
+ "PublishEventRequest",
50
+ "PublishEventResult",
51
+ "ResourceLimits",
52
+ "RetryDeliveryResult",
53
+ "RetryJob",
54
+ "RetryJobList",
55
+ "RetryPolicy",
56
+ "RetryPolicyList",
57
+ "Schedule",
58
+ "ScheduleList",
59
+ "SubscriptionQuota",
60
+ "Task",
61
+ "TaskList",
62
+ "UpdateEndpointRequest",
63
+ "UpdateRetryPolicyRequest",
64
+ "UpdateScheduleRequest",
65
+ "UsageRecords",
66
+ "WebhookEndpoint",
67
+ "__version__",
68
+ "is_api_error",
69
+ ]
edgecron/client.py ADDED
@@ -0,0 +1,57 @@
1
+ """Public client entrypoint for the EdgeCron SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Optional
7
+
8
+ import requests
9
+
10
+ from .deliveries import DeliveriesService
11
+ from .endpoints import EndpointsService
12
+ from .events import EventsService
13
+ from .retries import RetriesService
14
+ from .schedules import SchedulesService
15
+ from .subscription import SubscriptionService
16
+ from .tasks import TasksService
17
+ from .transport import DEFAULT_BASE_URL, SDK_VERSION, Transport
18
+
19
+ _KEY_ID_RE = re.compile(r"^ak_[0-9a-zA-Z_]+$")
20
+
21
+
22
+ class EdgeCron:
23
+ """EdgeCron API client.
24
+
25
+ The client is safe to reuse across requests. Create once and reuse.
26
+ """
27
+
28
+ version = SDK_VERSION
29
+
30
+ def __init__(
31
+ self,
32
+ key_id: str,
33
+ secret: str,
34
+ *,
35
+ base_url: str = DEFAULT_BASE_URL,
36
+ timeout: float = 30.0,
37
+ session: Optional[requests.Session] = None,
38
+ ) -> None:
39
+ if not _KEY_ID_RE.fullmatch(key_id):
40
+ raise ValueError(f"edgecron: key_id must match ak_<hex>, got: {key_id}")
41
+ if not secret:
42
+ raise ValueError("edgecron: secret must not be empty")
43
+
44
+ self._transport = Transport(
45
+ key_id=key_id,
46
+ secret=secret,
47
+ base_url=base_url,
48
+ timeout=timeout,
49
+ session=session,
50
+ )
51
+ self.schedules = SchedulesService(self._transport)
52
+ self.tasks = TasksService(self._transport)
53
+ self.events = EventsService(self._transport)
54
+ self.endpoints = EndpointsService(self._transport)
55
+ self.deliveries = DeliveriesService(self._transport)
56
+ self.retries = RetriesService(self._transport)
57
+ self.subscription = SubscriptionService(self._transport)
edgecron/deliveries.py ADDED
@@ -0,0 +1,37 @@
1
+ """Delivery APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .transport import Transport
8
+ from .types import Delivery, DeliveryList, RetryDeliveryResult
9
+ from .schedules import _page_query
10
+
11
+
12
+ class DeliveriesService:
13
+ def __init__(self, transport: Transport) -> None:
14
+ self._transport = transport
15
+
16
+ def list(
17
+ self,
18
+ page: int = 1,
19
+ page_size: int = 20,
20
+ status: str = "",
21
+ task_id: int = 0,
22
+ endpoint_id: int = 0,
23
+ ) -> DeliveryList:
24
+ q = _page_query(page, page_size)
25
+ if status:
26
+ q["status"] = status
27
+ if task_id > 0:
28
+ q["task_id"] = task_id
29
+ if endpoint_id > 0:
30
+ q["endpoint_id"] = endpoint_id
31
+ data = self._transport.request_json("GET", "/v1/deliveries", query=q)
32
+ items = [Delivery(**d) for d in (data.get("list") or [])]
33
+ return DeliveryList(total=data.get("total", 0), list=items)
34
+
35
+ def retry(self, id: int) -> RetryDeliveryResult:
36
+ data = self._transport.request_json("POST", f"/v1/deliveries/{id}/retry")
37
+ return RetryDeliveryResult(**data)
edgecron/endpoints.py ADDED
@@ -0,0 +1,43 @@
1
+ """Webhook endpoint APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .transport import Transport
8
+ from .types import CreateEndpointRequest, EndpointList, UpdateEndpointRequest, WebhookEndpoint
9
+ from .schedules import _compact, _page_query, _asdict
10
+
11
+
12
+ class EndpointsService:
13
+ def __init__(self, transport: Transport) -> None:
14
+ self._transport = transport
15
+
16
+ def create(self, req: CreateEndpointRequest) -> WebhookEndpoint:
17
+ data = self._transport.request_json("POST", "/v1/endpoints", body=_asdict(req))
18
+ return WebhookEndpoint(**data)
19
+
20
+ def get(self, id: int) -> WebhookEndpoint:
21
+ data = self._transport.request_json("GET", f"/v1/endpoints/{id}")
22
+ return WebhookEndpoint(**data)
23
+
24
+ def update(self, id: int, req: UpdateEndpointRequest) -> WebhookEndpoint:
25
+ data = self._transport.request_json("PATCH", f"/v1/endpoints/{id}", body=_compact(_asdict(req)))
26
+ return WebhookEndpoint(**data)
27
+
28
+ def list(self, page: int = 1, page_size: int = 20, status: str = "") -> EndpointList:
29
+ q = _page_query(page, page_size)
30
+ if status:
31
+ q["status"] = status
32
+ data = self._transport.request_json("GET", "/v1/endpoints", query=q)
33
+ items = [WebhookEndpoint(**ep) for ep in (data.get("list") or [])]
34
+ return EndpointList(total=data.get("total", 0), list=items)
35
+
36
+ def delete(self, id: int) -> None:
37
+ self._transport.request_json("DELETE", f"/v1/endpoints/{id}")
38
+
39
+ def enable(self, id: int) -> None:
40
+ self._transport.request_json("POST", f"/v1/endpoints/{id}/enable")
41
+
42
+ def disable(self, id: int) -> None:
43
+ self._transport.request_json("POST", f"/v1/endpoints/{id}/disable")
edgecron/error.py ADDED
@@ -0,0 +1,18 @@
1
+ """Error types for the EdgeCron SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class APIError(Exception):
7
+ """Business error returned by the EdgeCron API."""
8
+
9
+ def __init__(self, code: int, message: str, request_id: str) -> None:
10
+ self.code = code
11
+ self.message = message
12
+ self.request_id = request_id
13
+ super().__init__(f"edgecron: code={code} message={message} request_id={request_id}")
14
+
15
+
16
+ def is_api_error(exc: BaseException) -> bool:
17
+ """Return True when *exc* is an :class:`APIError`."""
18
+ return isinstance(exc, APIError)
edgecron/events.py ADDED
@@ -0,0 +1,41 @@
1
+ """Event APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .transport import Transport
8
+ from .types import Event, EventList, PublishEventRequest, PublishEventResult
9
+ from .schedules import _page_query, _asdict
10
+
11
+
12
+ class EventsService:
13
+ def __init__(self, transport: Transport) -> None:
14
+ self._transport = transport
15
+
16
+ def publish(self, req: PublishEventRequest) -> PublishEventResult:
17
+ data = self._transport.request_json("POST", "/v1/events", body=_asdict(req))
18
+ return PublishEventResult(**data)
19
+
20
+ def get(self, id: int) -> Event:
21
+ data = self._transport.request_json("GET", f"/v1/events/{id}")
22
+ return Event(**data)
23
+
24
+ def list(self, page: int = 1, page_size: int = 20, event_name: str = "", status: str = "") -> EventList:
25
+ q = _page_query(page, page_size)
26
+ if event_name:
27
+ q["event_name"] = event_name
28
+ if status:
29
+ q["status"] = status
30
+ data = self._transport.request_json("GET", "/v1/events", query=q)
31
+ items = [Event(**e) for e in (data.get("list") or [])]
32
+ return EventList(total=data.get("total", 0), list=items)
33
+
34
+ def enable(self, id: int) -> None:
35
+ self._transport.request_json("POST", f"/v1/events/{id}/enable")
36
+
37
+ def disable(self, id: int) -> None:
38
+ self._transport.request_json("POST", f"/v1/events/{id}/disable")
39
+
40
+ def delete(self, id: int) -> None:
41
+ self._transport.request_json("DELETE", f"/v1/events/{id}")
edgecron/retries.py ADDED
@@ -0,0 +1,60 @@
1
+ """Retry APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .transport import Transport
8
+ from .types import (
9
+ CreateRetryPolicyRequest,
10
+ RetryJob,
11
+ RetryJobList,
12
+ RetryPolicy,
13
+ RetryPolicyList,
14
+ UpdateRetryPolicyRequest,
15
+ )
16
+ from .schedules import _compact, _page_query, _asdict
17
+
18
+
19
+ class RetriesService:
20
+ def __init__(self, transport: Transport) -> None:
21
+ self._transport = transport
22
+
23
+ def create_policy(self, req: CreateRetryPolicyRequest) -> RetryPolicy:
24
+ data = self._transport.request_json("POST", "/v1/retries/policies", body=_asdict(req))
25
+ return RetryPolicy(**data)
26
+
27
+ def get_policy(self, id: int) -> RetryPolicy:
28
+ data = self._transport.request_json("GET", f"/v1/retries/policies/{id}")
29
+ return RetryPolicy(**data)
30
+
31
+ def list_policies(self) -> RetryPolicyList:
32
+ data = self._transport.request_json("GET", "/v1/retries/policies")
33
+ items = [RetryPolicy(**p) for p in (data.get("list") or [])]
34
+ return RetryPolicyList(total=data.get("total", 0), list=items)
35
+
36
+ def update_policy(self, id: int, req: UpdateRetryPolicyRequest) -> RetryPolicy:
37
+ data = self._transport.request_json("PATCH", f"/v1/retries/policies/{id}", body=_compact(_asdict(req)))
38
+ return RetryPolicy(**data)
39
+
40
+ def delete_policy(self, id: int) -> None:
41
+ self._transport.request_json("DELETE", f"/v1/retries/policies/{id}")
42
+
43
+ def list_jobs(
44
+ self,
45
+ page: int = 1,
46
+ page_size: int = 20,
47
+ status: str = "",
48
+ delivery_id: int = 0,
49
+ ) -> RetryJobList:
50
+ q = _page_query(page, page_size)
51
+ if status:
52
+ q["status"] = status
53
+ if delivery_id > 0:
54
+ q["delivery_id"] = delivery_id
55
+ data = self._transport.request_json("GET", "/v1/retries/jobs", query=q)
56
+ items = [RetryJob(**j) for j in (data.get("list") or [])]
57
+ return RetryJobList(total=data.get("total", 0), list=items)
58
+
59
+ def cancel_job(self, id: int) -> None:
60
+ self._transport.request_json("POST", f"/v1/retries/jobs/{id}/cancel")
edgecron/schedules.py ADDED
@@ -0,0 +1,61 @@
1
+ """Schedule APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .transport import Transport
8
+ from .types import CreateScheduleRequest, Schedule, ScheduleList, UpdateScheduleRequest
9
+
10
+
11
+ class SchedulesService:
12
+ def __init__(self, transport: Transport) -> None:
13
+ self._transport = transport
14
+
15
+ def create(self, req: CreateScheduleRequest) -> Schedule:
16
+ data = self._transport.request_json("POST", "/v1/schedules", body=_asdict(req))
17
+ return Schedule(**data)
18
+
19
+ def get(self, id: int) -> Schedule:
20
+ data = self._transport.request_json("GET", f"/v1/schedules/{id}")
21
+ return Schedule(**data)
22
+
23
+ def update(self, id: int, req: UpdateScheduleRequest) -> Schedule:
24
+ data = self._transport.request_json("PATCH", f"/v1/schedules/{id}", body=_compact(_asdict(req)))
25
+ return Schedule(**data)
26
+
27
+ def list(self, page: int = 1, page_size: int = 20, status: str = "") -> ScheduleList:
28
+ q = _page_query(page, page_size)
29
+ if status:
30
+ q["status"] = status
31
+ data = self._transport.request_json("GET", "/v1/schedules", query=q)
32
+ items = [Schedule(**s) for s in (data.get("list") or [])]
33
+ return ScheduleList(total=data.get("total", 0), list=items)
34
+
35
+ def delete(self, id: int) -> None:
36
+ self._transport.request_json("DELETE", f"/v1/schedules/{id}")
37
+
38
+ def pause(self, id: int) -> None:
39
+ self._transport.request_json("POST", f"/v1/schedules/{id}/pause")
40
+
41
+ def resume(self, id: int) -> None:
42
+ self._transport.request_json("POST", f"/v1/schedules/{id}/resume")
43
+
44
+
45
+ def _compact(d: dict) -> dict:
46
+ return {k: v for k, v in d.items() if v is not None}
47
+
48
+
49
+ def _page_query(page: int, page_size: int) -> dict:
50
+ if page <= 0:
51
+ page = 1
52
+ if page_size <= 0:
53
+ page_size = 20
54
+ page_size = min(page_size, 100)
55
+ return {"page": page, "page_size": page_size}
56
+
57
+
58
+ def _asdict(obj) -> dict:
59
+ if hasattr(obj, "__dataclass_fields__"):
60
+ return {f.name: getattr(obj, f.name) for f in obj.__dataclass_fields__.values()}
61
+ return dict(obj)
edgecron/signer.py ADDED
@@ -0,0 +1,31 @@
1
+ """HMAC-SHA256 signing for the EdgeCron SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ from typing import Mapping, Optional
8
+
9
+
10
+ def sign(secret: str, timestamp: str, query: Optional[Mapping[str, str]], body: bytes) -> str:
11
+ """Return the lowercase hex HMAC-SHA256 signature for a request.
12
+
13
+ Algorithm (matches Go reference implementation):
14
+ 1. Sort URL query parameters by key alphabetically.
15
+ 2. Join as: key1=value1&key2=value2
16
+ 3. If non-empty request body exists, append "&" + raw_body
17
+ 4. If no query params and no body, payload is empty string ""
18
+ 5. to_sign = unix_timestamp_string + "\\n" + payload
19
+ 6. signature = hex(HMAC-SHA256(secret_bytes, to_sign_bytes))
20
+ """
21
+ parts = []
22
+ if query:
23
+ for key in sorted(query):
24
+ parts.append(f"{key}={query[key]}")
25
+ payload = "&".join(parts)
26
+ if body:
27
+ body_text = body.decode("utf-8")
28
+ payload = f"{payload}&{body_text}" if payload else body_text
29
+ to_sign = f"{timestamp}\n{payload}"
30
+ digest = hmac.new(secret.encode("utf-8"), to_sign.encode("utf-8"), hashlib.sha256)
31
+ return digest.hexdigest()
@@ -0,0 +1,28 @@
1
+ """Subscription APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .transport import Transport
8
+ from .types import ResourceLimits, SubscriptionQuota, UsageRecords
9
+
10
+
11
+ class SubscriptionService:
12
+ def __init__(self, transport: Transport) -> None:
13
+ self._transport = transport
14
+
15
+ def quota(self) -> SubscriptionQuota:
16
+ data = self._transport.request_json("GET", "/v1/subscription/quota")
17
+ return SubscriptionQuota(**data)
18
+
19
+ def usage(self, period: str = "") -> UsageRecords:
20
+ q = {}
21
+ if period:
22
+ q["period"] = period
23
+ data = self._transport.request_json("GET", "/v1/subscription/usage", query=q)
24
+ return UsageRecords(**data)
25
+
26
+ def resource_limits(self) -> ResourceLimits:
27
+ data = self._transport.request_json("GET", "/v1/subscription/resource-limits")
28
+ return ResourceLimits(**data)
edgecron/tasks.py ADDED
@@ -0,0 +1,44 @@
1
+ """Task APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ from .transport import Transport
8
+ from .types import CreateTaskRequest, Task, TaskList
9
+ from .schedules import _page_query, _asdict
10
+
11
+
12
+ class TasksService:
13
+ def __init__(self, transport: Transport) -> None:
14
+ self._transport = transport
15
+
16
+ def create(self, req: CreateTaskRequest) -> Task:
17
+ data = self._transport.request_json("POST", "/v1/tasks", body=_asdict(req))
18
+ return Task(**data)
19
+
20
+ def get(self, id: int) -> Task:
21
+ data = self._transport.request_json("GET", f"/v1/tasks/{id}")
22
+ return Task(**data)
23
+
24
+ def list(
25
+ self,
26
+ page: int = 1,
27
+ page_size: int = 20,
28
+ status: str = "",
29
+ schedule_id: int = 0,
30
+ event_id: int = 0,
31
+ ) -> TaskList:
32
+ q = _page_query(page, page_size)
33
+ if status:
34
+ q["status"] = status
35
+ if schedule_id > 0:
36
+ q["schedule_id"] = schedule_id
37
+ if event_id > 0:
38
+ q["event_id"] = event_id
39
+ data = self._transport.request_json("GET", "/v1/tasks", query=q)
40
+ items = [Task(**t) for t in (data.get("list") or [])]
41
+ return TaskList(total=data.get("total", 0), list=items)
42
+
43
+ def cancel(self, id: int) -> None:
44
+ self._transport.request_json("POST", f"/v1/tasks/{id}/cancel")
edgecron/transport.py ADDED
@@ -0,0 +1,146 @@
1
+ """HTTP transport for the EdgeCron SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from typing import Any, Dict, Mapping, Optional
8
+ from urllib.parse import urlencode
9
+
10
+ import requests
11
+
12
+ from .error import APIError
13
+ from .signer import sign
14
+
15
+ DEFAULT_BASE_URL = "https://api.edgecron.com"
16
+ SDK_VERSION = "1.0.0"
17
+ MAX_RESPONSE_BYTES = 10 << 20
18
+
19
+
20
+ class Transport:
21
+ """Low-level HTTP transport shared by all services."""
22
+
23
+ def __init__(
24
+ self,
25
+ *,
26
+ key_id: str,
27
+ secret: str,
28
+ base_url: str = DEFAULT_BASE_URL,
29
+ timeout: float = 30.0,
30
+ session: Optional[requests.Session] = None,
31
+ user_agent: Optional[str] = None,
32
+ ) -> None:
33
+ self.key_id = key_id
34
+ self.secret = secret
35
+ self.base_url = base_url.rstrip("/")
36
+ self.timeout = timeout
37
+ if session is None:
38
+ self.session = requests.Session()
39
+ self.session.trust_env = False
40
+ else:
41
+ self.session = session
42
+ self.user_agent = user_agent or f"edgecron-python/{SDK_VERSION}"
43
+
44
+ def request_json(
45
+ self,
46
+ method: str,
47
+ path: str,
48
+ *,
49
+ query: Optional[Mapping[str, Any]] = None,
50
+ body: Optional[Mapping[str, Any]] = None,
51
+ ) -> Any:
52
+ body_bytes = b""
53
+ if body is not None:
54
+ body_bytes = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
55
+ return self._send(
56
+ method=method,
57
+ path=path,
58
+ query=self._normalize_query(query),
59
+ body_bytes=body_bytes,
60
+ content_type="application/json",
61
+ )
62
+
63
+ def _send(
64
+ self,
65
+ *,
66
+ method: str,
67
+ path: str,
68
+ query: Optional[Dict[str, str]],
69
+ body_bytes: bytes,
70
+ content_type: str,
71
+ ) -> Any:
72
+ timestamp = str(int(time.time()))
73
+ signature = sign(self.secret, timestamp, query, body_bytes)
74
+
75
+ url = self.base_url + path
76
+ if query:
77
+ url = f"{url}?{urlencode(sorted(query.items()))}"
78
+
79
+ req = requests.Request(method=method, url=url, data=body_bytes)
80
+ prepared = self.session.prepare_request(req)
81
+ prepared.headers["Content-Type"] = content_type
82
+ prepared.headers["X-Key-ID"] = self.key_id
83
+ prepared.headers["X-Timestamp"] = timestamp
84
+ prepared.headers["X-Signature"] = signature
85
+ prepared.headers["User-Agent"] = self.user_agent
86
+
87
+ response = self.session.send(prepared, timeout=self.timeout, stream=True)
88
+ raw = self._read_response(response)
89
+
90
+ if response.status_code < 200 or response.status_code >= 300:
91
+ api_error = self._maybe_api_error(raw)
92
+ if api_error is not None:
93
+ raise api_error
94
+ raise requests.HTTPError(
95
+ f"edgecron: http status {response.status_code}",
96
+ response=response,
97
+ )
98
+
99
+ try:
100
+ envelope = json.loads(raw.decode("utf-8"))
101
+ except json.JSONDecodeError as exc:
102
+ raise ValueError(f"edgecron: decode response (status={response.status_code}): {exc}") from exc
103
+
104
+ code = int(envelope.get("code", 0))
105
+ if code != 0:
106
+ raise APIError(
107
+ code,
108
+ str(envelope.get("message", "")),
109
+ str(envelope.get("request_id", "")),
110
+ )
111
+ return envelope.get("data")
112
+
113
+ def _read_response(self, response: requests.Response) -> bytes:
114
+ chunks = []
115
+ total = 0
116
+ try:
117
+ for chunk in response.iter_content(chunk_size=8192):
118
+ if not chunk:
119
+ continue
120
+ total += len(chunk)
121
+ if total > MAX_RESPONSE_BYTES:
122
+ raise ValueError("edgecron: response exceeds 10 MB limit")
123
+ chunks.append(chunk)
124
+ finally:
125
+ response.close()
126
+ return b"".join(chunks)
127
+
128
+ @staticmethod
129
+ def _maybe_api_error(raw: bytes) -> Optional[APIError]:
130
+ try:
131
+ envelope = json.loads(raw.decode("utf-8"))
132
+ except (UnicodeDecodeError, json.JSONDecodeError):
133
+ return None
134
+ code_value = envelope.get("code", 0)
135
+ if code_value in (None, ""):
136
+ return None
137
+ code = int(code_value)
138
+ if code == 0:
139
+ return None
140
+ return APIError(code, str(envelope.get("message", "")), str(envelope.get("request_id", "")))
141
+
142
+ @staticmethod
143
+ def _normalize_query(query: Optional[Mapping[str, Any]]) -> Optional[Dict[str, str]]:
144
+ if not query:
145
+ return None
146
+ return {str(key): str(value) for key, value in query.items()}
edgecron/types.py ADDED
@@ -0,0 +1,303 @@
1
+ """All DTO (data transfer object) types for the EdgeCron SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Dict, List, Optional
7
+
8
+
9
+ # --- Schedules ---
10
+
11
+
12
+ @dataclass
13
+ class Schedule:
14
+ id: int
15
+ app_id: str = ""
16
+ name: str = ""
17
+ cron_expr: str = ""
18
+ timezone: str = ""
19
+ payload: Optional[str] = None
20
+ status: str = ""
21
+ next_run_at: Optional[int] = None
22
+ endpoint_ids: List[int] = field(default_factory=list)
23
+ endpoint_names: Dict[int, str] = field(default_factory=dict)
24
+ created_at: int = 0
25
+ updated_at: int = 0
26
+
27
+
28
+ @dataclass
29
+ class CreateScheduleRequest:
30
+ name: str
31
+ cron_expr: str
32
+ timezone: str = "UTC"
33
+ payload: Optional[str] = None
34
+ endpoint_ids: Optional[List[int]] = None
35
+
36
+
37
+ @dataclass
38
+ class UpdateScheduleRequest:
39
+ name: Optional[str] = None
40
+ cron_expr: Optional[str] = None
41
+ timezone: Optional[str] = None
42
+ payload: Optional[str] = None
43
+ endpoint_ids: Optional[List[int]] = None
44
+
45
+
46
+ @dataclass
47
+ class ScheduleList:
48
+ total: int = 0
49
+ list: List[Schedule] = field(default_factory=list)
50
+
51
+
52
+ # --- Tasks ---
53
+
54
+
55
+ @dataclass
56
+ class Task:
57
+ id: int = 0
58
+ app_id: str = ""
59
+ schedule_id: Optional[int] = None
60
+ event_id: Optional[int] = None
61
+ endpoint_id: int = 0
62
+ task_type: str = ""
63
+ payload: Optional[str] = None
64
+ status: str = ""
65
+ run_at: Optional[int] = None
66
+ created_at: int = 0
67
+ updated_at: int = 0
68
+
69
+
70
+ @dataclass
71
+ class CreateTaskRequest:
72
+ endpoint_id: int
73
+ payload: Optional[str] = None
74
+ run_at: Optional[int] = None
75
+
76
+
77
+ @dataclass
78
+ class TaskList:
79
+ total: int = 0
80
+ list: List[Task] = field(default_factory=list)
81
+
82
+
83
+ # --- Events ---
84
+
85
+
86
+ @dataclass
87
+ class Event:
88
+ id: int = 0
89
+ app_id: str = ""
90
+ event_name: str = ""
91
+ event_key: str = ""
92
+ payload: Optional[str] = None
93
+ status: str = ""
94
+ created_at: int = 0
95
+
96
+
97
+ @dataclass
98
+ class PublishEventRequest:
99
+ event_name: str
100
+ event_key: str = ""
101
+ payload: Optional[str] = None
102
+
103
+
104
+ @dataclass
105
+ class PublishEventResult:
106
+ id: int = 0
107
+ app_id: str = ""
108
+ event_name: str = ""
109
+ event_key: str = ""
110
+ payload: Optional[str] = None
111
+ status: str = ""
112
+ fanout_count: int = 0
113
+ created_at: int = 0
114
+
115
+
116
+ @dataclass
117
+ class EventList:
118
+ total: int = 0
119
+ list: List[Event] = field(default_factory=list)
120
+
121
+
122
+ # --- Endpoints ---
123
+
124
+
125
+ @dataclass
126
+ class WebhookEndpoint:
127
+ id: int = 0
128
+ app_id: str = ""
129
+ name: str = ""
130
+ url: str = ""
131
+ method: str = ""
132
+ headers: Optional[str] = None
133
+ secret: Optional[str] = None
134
+ timeout_ms: int = 0
135
+ retry_policy_id: Optional[int] = None
136
+ filter_events: Optional[str] = None
137
+ status: str = ""
138
+ created_at: int = 0
139
+ updated_at: int = 0
140
+
141
+
142
+ @dataclass
143
+ class CreateEndpointRequest:
144
+ name: str
145
+ url: str
146
+ method: str = "POST"
147
+ headers: Optional[str] = None
148
+ secret: Optional[str] = None
149
+ timeout_ms: Optional[int] = None
150
+ retry_policy_id: Optional[int] = None
151
+ filter_events: Optional[str] = None
152
+
153
+
154
+ @dataclass
155
+ class UpdateEndpointRequest:
156
+ name: Optional[str] = None
157
+ url: Optional[str] = None
158
+ method: Optional[str] = None
159
+ headers: Optional[str] = None
160
+ secret: Optional[str] = None
161
+ timeout_ms: Optional[int] = None
162
+ retry_policy_id: Optional[int] = None
163
+ filter_events: Optional[str] = None
164
+
165
+
166
+ @dataclass
167
+ class EndpointList:
168
+ total: int = 0
169
+ list: List[WebhookEndpoint] = field(default_factory=list)
170
+
171
+
172
+ # --- Deliveries ---
173
+
174
+
175
+ @dataclass
176
+ class Delivery:
177
+ id: int = 0
178
+ app_id: str = ""
179
+ task_id: int = 0
180
+ endpoint_id: int = 0
181
+ attempt: int = 0
182
+ status: str = ""
183
+ http_status: Optional[int] = None
184
+ latency_ms: int = 0
185
+ request_body_hash: str = ""
186
+ error_message: Optional[str] = None
187
+ next_retry_at: Optional[int] = None
188
+ created_at: int = 0
189
+ updated_at: int = 0
190
+
191
+
192
+ @dataclass
193
+ class DeliveryList:
194
+ total: int = 0
195
+ list: List[Delivery] = field(default_factory=list)
196
+
197
+
198
+ @dataclass
199
+ class RetryDeliveryResult:
200
+ delivery_id: int = 0
201
+ retry_job_id: int = 0
202
+ status: str = ""
203
+
204
+
205
+ # --- Retries ---
206
+
207
+
208
+ @dataclass
209
+ class RetryPolicy:
210
+ id: int = 0
211
+ app_id: str = ""
212
+ name: str = ""
213
+ max_attempts: int = 0
214
+ backoff_type: str = ""
215
+ initial_delay_sec: int = 0
216
+ max_delay_sec: int = 0
217
+ status: str = ""
218
+ created_at: int = 0
219
+ updated_at: int = 0
220
+
221
+
222
+ @dataclass
223
+ class CreateRetryPolicyRequest:
224
+ name: str
225
+ max_attempts: int = 3
226
+ backoff_type: str = "exponential"
227
+ initial_delay_sec: int = 60
228
+ max_delay_sec: int = 3600
229
+
230
+
231
+ @dataclass
232
+ class UpdateRetryPolicyRequest:
233
+ name: Optional[str] = None
234
+ max_attempts: Optional[int] = None
235
+ backoff_type: Optional[str] = None
236
+ initial_delay_sec: Optional[int] = None
237
+ max_delay_sec: Optional[int] = None
238
+ status: Optional[str] = None
239
+
240
+
241
+ @dataclass
242
+ class RetryPolicyList:
243
+ total: int = 0
244
+ list: List[RetryPolicy] = field(default_factory=list)
245
+
246
+
247
+ @dataclass
248
+ class RetryJob:
249
+ id: int = 0
250
+ app_id: str = ""
251
+ delivery_id: int = 0
252
+ attempt: int = 0
253
+ status: str = ""
254
+ run_at: Optional[int] = None
255
+ locked_until: Optional[int] = None
256
+ last_error: Optional[str] = None
257
+ created_at: int = 0
258
+ updated_at: int = 0
259
+
260
+
261
+ @dataclass
262
+ class RetryJobList:
263
+ total: int = 0
264
+ list: List[RetryJob] = field(default_factory=list)
265
+
266
+
267
+ # --- Subscription ---
268
+
269
+
270
+ @dataclass
271
+ class SubscriptionQuota:
272
+ plan_code: str = ""
273
+ billing_cycle: str = ""
274
+ quota: int = 0
275
+ used: int = 0
276
+ remaining: int = 0
277
+ exceeded: bool = False
278
+ current_period_start: int = 0
279
+ current_period_end: int = 0
280
+ usage_percent: float = 0.0
281
+
282
+
283
+ @dataclass
284
+ class UsageRecordItem:
285
+ event_type: str = ""
286
+ period: str = ""
287
+ count: int = 0
288
+
289
+
290
+ @dataclass
291
+ class UsageRecords:
292
+ period: str = ""
293
+ total_events: int = 0
294
+ items: List[UsageRecordItem] = field(default_factory=list)
295
+
296
+
297
+ @dataclass
298
+ class ResourceLimits:
299
+ max_cron_jobs: int = 0
300
+ current_cron_jobs: int = 0
301
+ max_endpoints: int = 0
302
+ current_endpoints: int = 0
303
+ log_retention_days: int = 0
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: edgecron-python
3
+ Version: 1.0.0
4
+ Summary: Official Python SDK for the EdgeCron webhook scheduling and callback delivery platform.
5
+ Project-URL: Homepage, https://github.com/edgecron/edgecron-python
6
+ Project-URL: Repository, https://github.com/edgecron/edgecron-python
7
+ Project-URL: Documentation, https://www.edgecron.com
8
+ Author: EdgeCron Team
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: callback,delivery,edgecron,scheduling,webhook
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: requests<3,>=2.31.0
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest<9,>=8; extra == 'test'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # EdgeCron Python SDK
30
+
31
+ Official Python SDK for the EdgeCron webhook scheduling and callback delivery platform.
32
+
33
+ Schedule delayed HTTP requests, deliver webhooks reliably, and automatically retry failed calls — with full execution history so nothing gets lost.
34
+
35
+ 中文文档:[README.zh-CN.md](README.zh-CN.md)
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install edgecron-python
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```python
46
+ from edgecron import EdgeCron, APIError
47
+ from edgecron.types import CreateScheduleRequest
48
+
49
+ client = EdgeCron("ak_xxx", "sk_xxx")
50
+
51
+ try:
52
+ schedule = client.schedules.create(
53
+ CreateScheduleRequest(
54
+ name="my-schedule",
55
+ cron_expr="*/5 * * * *",
56
+ )
57
+ )
58
+ print(schedule.id)
59
+ except APIError as exc:
60
+ print(exc.code, exc.message, exc.request_id)
61
+ ```
62
+
63
+ ## Modules
64
+
65
+ | Client method | Description |
66
+ |----------------------------|----------------------------------|
67
+ | `client.schedules.*` | Cron schedule CRUD, pause, resume |
68
+ | `client.tasks.*` | Task execution instances, cancel |
69
+ | `client.events.*` | Event publishing and management |
70
+ | `client.endpoints.*` | Webhook endpoint configuration |
71
+ | `client.deliveries.*` | Delivery attempt records and retry |
72
+ | `client.retries.*` | Retry policies and jobs |
73
+ | `client.subscription.*` | Quota, usage, and resource limits |
74
+
75
+ ## Configuration
76
+
77
+ - `base_url` — override API base URL
78
+ - `timeout` — HTTP client timeout in seconds
79
+ - `session` — custom `requests.Session`
80
+
81
+ ## Error Handling
82
+
83
+ Service-side business errors raise `APIError`.
84
+
85
+ ```python
86
+ from edgecron import APIError
87
+
88
+ try:
89
+ client.schedules.get(123)
90
+ except APIError as exc:
91
+ print(exc.code, exc.message, exc.request_id)
92
+ ```
93
+
94
+ ## Security Notice
95
+
96
+ This is a server-side SDK. Never expose `secret` in browsers, mobile apps, or other untrusted clients.
@@ -0,0 +1,17 @@
1
+ edgecron/__init__.py,sha256=EpgWHS-kTSBTC93-ur8vGrQMi2iOOkdATKl-n-aWQeQ,1383
2
+ edgecron/client.py,sha256=ay9dOdDdahm8Bx4bekyjRAUcaaUbIc_nj4HIk7q19UA,1722
3
+ edgecron/deliveries.py,sha256=ePJMlupRisSMzA8JNBehst6ASVfoQB6bvtQExe7nq7M,1135
4
+ edgecron/endpoints.py,sha256=4JARU8NWhldGQH9GUn_nmfhi12NEqPs5gh7KknABnFw,1720
5
+ edgecron/error.py,sha256=dEqXWIJpqA0ViEfGjQyVvJdRQo-ODZ6qii_wsghCa2M,564
6
+ edgecron/events.py,sha256=VvAn-24Zbb7TFteU-y7HtkjfDikV2KeZBkjdLOpEZFo,1490
7
+ edgecron/retries.py,sha256=CgEByGc4IV1SXe3c4awTVrfD8HyoDq01AxJ7G7yoyu4,2096
8
+ edgecron/schedules.py,sha256=JQFW-OAqiBrHt0X3vOHjDUpqrQkTjWGp8dEfNgYpNF4,2090
9
+ edgecron/signer.py,sha256=S2k--UHcbIdxiU5wYPfkTU7_ot1uqmDBMQfSRX5n0t4,1157
10
+ edgecron/subscription.py,sha256=4iIX2XEajqIakw2O8roP-sJpFLfQ0r_BzWJhRjIXCqw,891
11
+ edgecron/tasks.py,sha256=22QxyK4UD_cdwlx0RSexhVb9qb3zabT6OGbRheIAmcY,1346
12
+ edgecron/transport.py,sha256=EEZ5WX7V_mR-UW2iiduRgs4EUS8WWpiNdA5amixfnr0,4680
13
+ edgecron/types.py,sha256=KM9htPSWSBbAHIa94fJwt4anqYe3zf5PiJJUfm9qTCc,6038
14
+ edgecron_python-1.0.0.dist-info/METADATA,sha256=Y5tQb2U1zmCwKBQLwIXifpQTWmK-qNgx79_5BOM3rg0,3082
15
+ edgecron_python-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
16
+ edgecron_python-1.0.0.dist-info/licenses/LICENSE,sha256=y3l8dQw2DbEPg-_RGDxGoA0Xa2SZaLTJNvxhfXMA0Kk,1070
17
+ edgecron_python-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EdgeCron Team
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.