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 +69 -0
- edgecron/client.py +57 -0
- edgecron/deliveries.py +37 -0
- edgecron/endpoints.py +43 -0
- edgecron/error.py +18 -0
- edgecron/events.py +41 -0
- edgecron/retries.py +60 -0
- edgecron/schedules.py +61 -0
- edgecron/signer.py +31 -0
- edgecron/subscription.py +28 -0
- edgecron/tasks.py +44 -0
- edgecron/transport.py +146 -0
- edgecron/types.py +303 -0
- edgecron_python-1.0.0.dist-info/METADATA +96 -0
- edgecron_python-1.0.0.dist-info/RECORD +17 -0
- edgecron_python-1.0.0.dist-info/WHEEL +4 -0
- edgecron_python-1.0.0.dist-info/licenses/LICENSE +21 -0
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()
|
edgecron/subscription.py
ADDED
|
@@ -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,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.
|