server4agent 0.1.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.
- server4agent/__init__.py +79 -0
- server4agent/_async/__init__.py +0 -0
- server4agent/_async/client.py +559 -0
- server4agent/_async/transport.py +76 -0
- server4agent/_models.py +197 -0
- server4agent/_sync/__init__.py +0 -0
- server4agent/_sync/client.py +567 -0
- server4agent/_sync/transport.py +75 -0
- server4agent/_transport.py +115 -0
- server4agent/_version.py +1 -0
- server4agent/errors.py +103 -0
- server4agent/py.typed +0 -0
- server4agent/webhooks.py +59 -0
- server4agent-0.1.0.dist-info/METADATA +151 -0
- server4agent-0.1.0.dist-info/RECORD +17 -0
- server4agent-0.1.0.dist-info/WHEEL +4 -0
- server4agent-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Transport pieces shared by the sync and async clients.
|
|
2
|
+
|
|
3
|
+
Only the actual send/await differs between them; URL/header prep, the retry
|
|
4
|
+
policy, JSON decoding, error parsing, and debug logging all live here so the
|
|
5
|
+
two clients stay in lockstep.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import random
|
|
13
|
+
from typing import Any, Mapping, Optional
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
from .errors import error_from_status
|
|
18
|
+
|
|
19
|
+
DEFAULT_BASE_URL = "https://api.server4agent.com"
|
|
20
|
+
DEFAULT_TIMEOUT = 60.0
|
|
21
|
+
DEFAULT_MAX_RETRIES = 2
|
|
22
|
+
|
|
23
|
+
USER_AGENT = f"server4agent-python/{__version__}"
|
|
24
|
+
|
|
25
|
+
# Statuses worth retrying at all.
|
|
26
|
+
RETRYABLE_STATUS = frozenset({408, 429, 500, 502, 503, 504})
|
|
27
|
+
# Methods safe to retry on any retryable status (no risk of duplicate writes).
|
|
28
|
+
IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "PUT", "DELETE", "OPTIONS"})
|
|
29
|
+
# For non-idempotent methods (POST/PATCH) we only retry when the server clearly
|
|
30
|
+
# did not process the request, so a retry can never create a duplicate.
|
|
31
|
+
NON_IDEMPOTENT_RETRY_STATUS = frozenset({408, 429, 503})
|
|
32
|
+
|
|
33
|
+
_BACKOFF_BASE = 0.5
|
|
34
|
+
_BACKOFF_MAX = 8.0
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("server4agent")
|
|
37
|
+
# Opt-in one-line request logging via SERVER4AGENT_LOG=debug.
|
|
38
|
+
if os.environ.get("SERVER4AGENT_LOG", "").lower() == "debug" and not logger.handlers:
|
|
39
|
+
_h = logging.StreamHandler()
|
|
40
|
+
_h.setFormatter(logging.Formatter("[server4agent] %(message)s"))
|
|
41
|
+
logger.addHandler(_h)
|
|
42
|
+
logger.setLevel(logging.DEBUG)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def default_headers(api_key: str) -> dict:
|
|
46
|
+
return {
|
|
47
|
+
"Authorization": f"Bearer {api_key}",
|
|
48
|
+
"User-Agent": USER_AGENT,
|
|
49
|
+
"Accept": "application/json",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def quote_segment(value: Any) -> str:
|
|
54
|
+
"""Percent-encode a value for safe use as a single URL path segment, so an id
|
|
55
|
+
containing "/", "..", "?", or "#" can't redirect the request elsewhere."""
|
|
56
|
+
return quote(str(value), safe="")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def clean_params(query: Optional[Mapping[str, Any]]) -> Optional[dict]:
|
|
60
|
+
"""Drop None values so we never send `?path=None`."""
|
|
61
|
+
if not query:
|
|
62
|
+
return None
|
|
63
|
+
out = {k: v for k, v in query.items() if v is not None}
|
|
64
|
+
return out or None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def decode_json(raw: bytes) -> Any:
|
|
68
|
+
"""Parse a JSON body, tolerating empty or non-JSON responses (proxy HTML, etc.)."""
|
|
69
|
+
if not raw:
|
|
70
|
+
return None
|
|
71
|
+
try:
|
|
72
|
+
return json.loads(raw)
|
|
73
|
+
except (json.JSONDecodeError, ValueError):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def should_retry_status(method: str, status: int, attempt: int, max_retries: int) -> bool:
|
|
78
|
+
if attempt >= max_retries or status not in RETRYABLE_STATUS:
|
|
79
|
+
return False
|
|
80
|
+
if method.upper() in IDEMPOTENT_METHODS:
|
|
81
|
+
return True
|
|
82
|
+
return status in NON_IDEMPOTENT_RETRY_STATUS
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def can_retry_exception(method: str, attempt: int, max_retries: int) -> bool:
|
|
86
|
+
# A network/timeout error may have been processed server-side, so only retry
|
|
87
|
+
# methods where a duplicate is harmless.
|
|
88
|
+
return attempt < max_retries and method.upper() in IDEMPOTENT_METHODS
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def retry_delay(attempt: int, retry_after: Optional[str]) -> float:
|
|
92
|
+
"""Seconds to wait before the next attempt.
|
|
93
|
+
|
|
94
|
+
Honors a numeric ``Retry-After`` header when present; otherwise uses
|
|
95
|
+
exponential backoff with full jitter to avoid thundering herds.
|
|
96
|
+
"""
|
|
97
|
+
if retry_after:
|
|
98
|
+
try:
|
|
99
|
+
return min(float(retry_after), _BACKOFF_MAX)
|
|
100
|
+
except ValueError:
|
|
101
|
+
pass # HTTP-date form — fall through to backoff
|
|
102
|
+
ceiling = min(_BACKOFF_MAX, _BACKOFF_BASE * (2 ** attempt))
|
|
103
|
+
return random.uniform(0, ceiling)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def request_id_from(headers: Mapping[str, str]) -> Optional[str]:
|
|
107
|
+
return headers.get("x-request-id") or headers.get("X-Request-Id")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def raise_for_status(status: int, body: Any, headers: Mapping[str, str]) -> None:
|
|
111
|
+
"""Turn a non-2xx response into the right typed error."""
|
|
112
|
+
parsed = body if isinstance(body, dict) else {}
|
|
113
|
+
code = parsed.get("error", "unknown_error")
|
|
114
|
+
message = parsed.get("message") or f"Request failed with status {status}"
|
|
115
|
+
raise error_from_status(status, code, message, request_id_from(headers))
|
server4agent/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
server4agent/errors.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Server4AgentError(Exception):
|
|
7
|
+
"""Base class for every error raised by this SDK."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIConnectionError(Server4AgentError):
|
|
11
|
+
"""The request never reached the API (DNS, TCP, TLS, or a dropped connection)."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str = "Could not reach the Server4Agent API.") -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.message = message
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class APITimeoutError(APIConnectionError):
|
|
19
|
+
"""The request timed out before the API responded."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str = "Request to the Server4Agent API timed out.") -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIStatusError(Server4AgentError):
|
|
26
|
+
"""The API returned a 4xx/5xx response.
|
|
27
|
+
|
|
28
|
+
Prefer catching a specific subclass (``NotFoundError``, ``RateLimitError``,
|
|
29
|
+
...). ``code`` is the machine-readable string from the response body and
|
|
30
|
+
``request_id`` (when present) is worth quoting to support.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
status: int,
|
|
36
|
+
code: str,
|
|
37
|
+
message: str,
|
|
38
|
+
request_id: Optional[str] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.status = status
|
|
42
|
+
self.code = code
|
|
43
|
+
self.message = message
|
|
44
|
+
self.request_id = request_id
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
return (
|
|
48
|
+
f"{type(self).__name__}(status={self.status!r}, code={self.code!r}, "
|
|
49
|
+
f"message={self.message!r}, request_id={self.request_id!r})"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BadRequestError(APIStatusError):
|
|
54
|
+
"""400 — the request was malformed."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AuthenticationError(APIStatusError):
|
|
58
|
+
"""401 — the API key is missing, invalid, or revoked."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PermissionDeniedError(APIStatusError):
|
|
62
|
+
"""403 — the key is valid but not scoped to this resource."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class NotFoundError(APIStatusError):
|
|
66
|
+
"""404 — no such resource."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ConflictError(APIStatusError):
|
|
70
|
+
"""409 — the request conflicts with the current state (e.g. a cap was hit)."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class UnprocessableEntityError(APIStatusError):
|
|
74
|
+
"""422 — the request was well-formed but semantically invalid."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RateLimitError(APIStatusError):
|
|
78
|
+
"""429 — too many requests. The SDK retries these automatically; you only see it once retries are exhausted."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class InternalServerError(APIStatusError):
|
|
82
|
+
"""5xx — something went wrong on the API side."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_STATUS_TO_ERROR = {
|
|
86
|
+
400: BadRequestError,
|
|
87
|
+
401: AuthenticationError,
|
|
88
|
+
403: PermissionDeniedError,
|
|
89
|
+
404: NotFoundError,
|
|
90
|
+
409: ConflictError,
|
|
91
|
+
422: UnprocessableEntityError,
|
|
92
|
+
429: RateLimitError,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def error_from_status(
|
|
97
|
+
status: int, code: str, message: str, request_id: Optional[str] = None
|
|
98
|
+
) -> APIStatusError:
|
|
99
|
+
"""Map an HTTP status onto the most specific ``APIStatusError`` subclass."""
|
|
100
|
+
cls = _STATUS_TO_ERROR.get(status)
|
|
101
|
+
if cls is None:
|
|
102
|
+
cls = InternalServerError if status >= 500 else APIStatusError
|
|
103
|
+
return cls(status, code, message, request_id)
|
server4agent/py.typed
ADDED
|
File without changes
|
server4agent/webhooks.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
# Server4Agent signs webhook deliveries as
|
|
9
|
+
# HMAC-SHA256(secret, f"{unix_ts}.{raw_body}"), sent as the
|
|
10
|
+
# Server4Agent-Signature header: "t=<unix_ts>,v1=<hex digest>".
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _sign(secret: str, body: str, timestamp: int) -> str:
|
|
14
|
+
digest = hmac.new(
|
|
15
|
+
secret.encode("utf-8"),
|
|
16
|
+
f"{timestamp}.{body}".encode("utf-8"),
|
|
17
|
+
hashlib.sha256,
|
|
18
|
+
).hexdigest()
|
|
19
|
+
return f"t={timestamp},v1={digest}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def verify_webhook_signature(
|
|
23
|
+
secret: str,
|
|
24
|
+
body: str,
|
|
25
|
+
header: Optional[str],
|
|
26
|
+
tolerance_sec: int = 300,
|
|
27
|
+
) -> bool:
|
|
28
|
+
"""Verify a webhook delivery.
|
|
29
|
+
|
|
30
|
+
`body` must be the raw request body exactly as received — decoding and
|
|
31
|
+
re-serializing JSON changes the bytes and breaks the signature. `header`
|
|
32
|
+
is the `Server4Agent-Signature` header value. Deliveries older than
|
|
33
|
+
`tolerance_sec` (default 300s) are rejected to limit replay windows.
|
|
34
|
+
"""
|
|
35
|
+
if not header:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
parts = {}
|
|
39
|
+
for piece in header.split(","):
|
|
40
|
+
if "=" not in piece:
|
|
41
|
+
continue
|
|
42
|
+
k, v = piece.split("=", 1)
|
|
43
|
+
parts[k] = v
|
|
44
|
+
|
|
45
|
+
raw_ts = parts.get("t")
|
|
46
|
+
v1 = parts.get("v1")
|
|
47
|
+
if not raw_ts or not v1:
|
|
48
|
+
return False
|
|
49
|
+
try:
|
|
50
|
+
timestamp = int(raw_ts)
|
|
51
|
+
except ValueError:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
if abs(time.time() - timestamp) > tolerance_sec:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
expected = _sign(secret, body, timestamp)
|
|
58
|
+
expected_v1 = expected.split("v1=")[1]
|
|
59
|
+
return hmac.compare_digest(expected_v1, v1)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: server4agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Server4Agent REST API — sync + async, typed, with retries.
|
|
5
|
+
Project-URL: Homepage, https://www.server4agent.com
|
|
6
|
+
Project-URL: Documentation, https://www.server4agent.com/docs/sdks
|
|
7
|
+
Project-URL: Repository, https://github.com/Server4Agent/server4agent-python
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,mcp,sdk,server4agent
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: httpx<1,>=0.24
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# server4agent
|
|
24
|
+
|
|
25
|
+
Python client for the [Server4Agent](https://www.server4agent.com) REST API —
|
|
26
|
+
sync **and** async, fully typed, with automatic retries.
|
|
27
|
+
|
|
28
|
+
This SDK is for the code *around* your agent — your backend, a script, a
|
|
29
|
+
notebook, a webhook receiver. If your agent itself does tool-calling, point it
|
|
30
|
+
at the [MCP server](https://www.server4agent.com/docs/mcp) directly; it doesn't
|
|
31
|
+
need this package.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install server4agent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires Python 3.9+. Depends only on [`httpx`](https://www.python-httpx.org/).
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from server4agent import Server4Agent
|
|
45
|
+
|
|
46
|
+
client = Server4Agent() # reads SERVER4AGENT_API_KEY from the environment
|
|
47
|
+
|
|
48
|
+
# create() returns a handle you can act on directly
|
|
49
|
+
server = client.servers.create(tier="small")
|
|
50
|
+
|
|
51
|
+
# kick off a build and block until it's live
|
|
52
|
+
build = server.builds.start("a FastAPI todo API with a web UI").wait()
|
|
53
|
+
print(build.url)
|
|
54
|
+
|
|
55
|
+
# run a command right on the server
|
|
56
|
+
print(server.exec("ls -la"))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`api_key` can also be passed explicitly: `Server4Agent(api_key="sk_live_...")`.
|
|
60
|
+
Use it as a context manager to close the connection pool:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
with Server4Agent() as client:
|
|
64
|
+
...
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Async
|
|
68
|
+
|
|
69
|
+
The async client mirrors the sync one method-for-method:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import asyncio
|
|
73
|
+
from server4agent import AsyncServer4Agent
|
|
74
|
+
|
|
75
|
+
async def main():
|
|
76
|
+
async with AsyncServer4Agent() as client:
|
|
77
|
+
server = await client.servers.create(tier="small")
|
|
78
|
+
task = await server.tasks.create("Scrape today's HN front page to JSON.")
|
|
79
|
+
await task.wait()
|
|
80
|
+
print(task.status, task.result)
|
|
81
|
+
|
|
82
|
+
asyncio.run(main())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Handles
|
|
86
|
+
|
|
87
|
+
`create()` and `get()` return rich handles — data records you can also act on:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
server = client.servers.get("srv_abc")
|
|
91
|
+
server.tasks.create("...") # sub-resources are scoped to the server
|
|
92
|
+
server.files.write("app.py", "...")
|
|
93
|
+
server.deploy()
|
|
94
|
+
server.refresh() # re-fetch in place
|
|
95
|
+
|
|
96
|
+
project = client.projects.get("prj_xyz")
|
|
97
|
+
project.update(visibility="public")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`task.wait()` / `build.wait()` poll until a terminal state; both accept
|
|
101
|
+
`poll_interval` and `timeout` (seconds).
|
|
102
|
+
|
|
103
|
+
## Errors
|
|
104
|
+
|
|
105
|
+
Every failure is a subclass of `Server4AgentError`, so you can catch broadly or
|
|
106
|
+
narrowly:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from server4agent import NotFoundError, RateLimitError, APIStatusError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
client.servers.get("srv_missing")
|
|
113
|
+
except NotFoundError:
|
|
114
|
+
... # 404
|
|
115
|
+
except RateLimitError as e:
|
|
116
|
+
... # 429 (already retried; quote e.request_id to support)
|
|
117
|
+
except APIStatusError as e:
|
|
118
|
+
print(e.status, e.code, e.message, e.request_id)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Retryable failures (429, 5xx, network blips) are retried automatically with
|
|
122
|
+
exponential backoff. Tune it per client:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
Server4Agent(timeout=30.0, max_retries=4)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Verifying webhooks
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
import os
|
|
132
|
+
from server4agent import verify_webhook_signature
|
|
133
|
+
|
|
134
|
+
# request.data must be the raw body — parse it as JSON only after verifying.
|
|
135
|
+
ok = verify_webhook_signature(
|
|
136
|
+
secret=os.environ["SERVER4AGENT_WEBHOOK_SECRET"],
|
|
137
|
+
body=request.data.decode("utf-8"),
|
|
138
|
+
header=request.headers.get("Server4Agent-Signature"),
|
|
139
|
+
)
|
|
140
|
+
if not ok:
|
|
141
|
+
return "invalid signature", 401
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## API surface
|
|
145
|
+
|
|
146
|
+
`servers`, `projects`, `templates`, `tasks`, `builds`, `files`, `keys`,
|
|
147
|
+
`webhooks` — see the [REST API docs](https://www.server4agent.com/docs/api) for
|
|
148
|
+
the endpoints each method wraps.
|
|
149
|
+
|
|
150
|
+
Server-side only: this SDK holds your `sk_live_` key. Never embed it in a
|
|
151
|
+
notebook or script you share.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
server4agent/__init__.py,sha256=_D-aXt-Krlyj1yotPeEl2ca3J0w_2ccAR9Cu2GZR4sY,1632
|
|
2
|
+
server4agent/_models.py,sha256=QedyzzguyqYur_gVim7TYTm_VtBAQIUeXee0S9kp7m8,5520
|
|
3
|
+
server4agent/_transport.py,sha256=6UhDhdd74Vpo39Ju2oQJDodshwe8DrySciL01DMdQuI,4088
|
|
4
|
+
server4agent/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
5
|
+
server4agent/errors.py,sha256=gvyF5DyfE76vCMgOZNmVftDaKZUS9RhahhHgolR9MQU,3007
|
|
6
|
+
server4agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
server4agent/webhooks.py,sha256=NqSESJ2AeQ2R_4fqwCruA6u93qPNSTmPgHJYLLq-H1g,1589
|
|
8
|
+
server4agent/_async/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
server4agent/_async/client.py,sha256=9nvwd8aCPgi4OK6HISD3FJkKi9WQAiNGwNbi2aZXdIE,20145
|
|
10
|
+
server4agent/_async/transport.py,sha256=8giEr6CA27-4r-AATLuVVdebCwYECx_xKnx2L-o6ZL0,2638
|
|
11
|
+
server4agent/_sync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
server4agent/_sync/client.py,sha256=1kKRdLiYuRwCOHFOVFBF7684873DhPppU7tBA9SRt1w,19838
|
|
13
|
+
server4agent/_sync/transport.py,sha256=Wbxer1dXRMomeN3l814ZUBdmD9ZWr-zDEKJ7yRVjUpw,2562
|
|
14
|
+
server4agent-0.1.0.dist-info/METADATA,sha256=OGDblXPEsytSRUzgoSqtNs6_7EZkKux0nlAHDJAapSE,4365
|
|
15
|
+
server4agent-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
16
|
+
server4agent-0.1.0.dist-info/licenses/LICENSE,sha256=mn_K4ahqVsYCCH2u2DsU7nUWuhYa2Qza07_TnTqYPSE,1069
|
|
17
|
+
server4agent-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Server4Agent
|
|
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.
|