flyewin 0.1.0__tar.gz

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.
flyewin-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.1
2
+ Name: flyewin
3
+ Version: 0.1.0
4
+ Summary: Official client for the flye.win APIs. No dependencies.
5
+ License: MIT
6
+ Project-URL: Homepage, https://flye.win
7
+ Project-URL: Documentation, https://flye.win/docs
8
+ Keywords: flye,flye.win,api,sdk
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # flyewin
17
+
18
+ Official Python client for the [flye.win](https://flye.win) APIs. No dependencies, Python 3.8+.
19
+
20
+ ```sh
21
+ pip install flyewin
22
+ ```
23
+
24
+ ```python
25
+ from flyewin import Flye
26
+
27
+ flye = Flye() # reads FLYE_API_KEY, or Flye("flye_sk_...")
28
+
29
+ print(flye.utils.uuid(count=2))
30
+ ```
31
+
32
+ ## Test keys
33
+
34
+ Keys that start with `flye_test_` answer with the documented examples, need no plan and
35
+ use no quota (30 requests/minute, 1,000/day). Use them while building and in CI, then
36
+ switch to a `flye_sk_` key. Create both at https://flye.win/dashboard/keys.
37
+
38
+ ## Errors
39
+
40
+ Every failure raises `FlyeError` with a stable `code`:
41
+
42
+ ```python
43
+ from flyewin import Flye, FlyeError
44
+
45
+ try:
46
+ flye.utils.hash(text="hi")
47
+ except FlyeError as err:
48
+ if err.code == "rate_limited":
49
+ ... # err.status == 429
50
+ ```
51
+
52
+ Network errors and 5xx on GET requests are retried twice with backoff, and so is a `429`
53
+ with a short `Retry-After`. Change it with `Flye(max_retries=0)`.
54
+
55
+ Rate limit and quota headers of the last call are in `flye.last_response.headers`
56
+ (`x-ratelimit-remaining`, `x-quota-remaining`).
57
+
58
+ ## Options
59
+
60
+ `Flye(api_key=None, *, base_url="https://flye.win/v1", timeout=30.0, max_retries=2)`
61
+
62
+ Every method also takes `timeout=` for that call.
63
+
64
+ ## Methods
65
+
66
+ ### Stickers — `flye.stickers`
67
+
68
+ Search stickers and animated GIFs, with ready-to-use links. Docs: https://flye.win/apis/stickers
69
+
70
+ | Method | HTTP | |
71
+ | ------ | ---- | - |
72
+ | `stickers.search()` | `GET /stickers/search` | Search stickers |
73
+ | `stickers.list_packs(q="gato")` | `GET /stickers/packs` | List packs |
74
+ | `stickers.get_pack("...")` | `GET /stickers/packs/{id}` | Stickers in a pack |
75
+
76
+ ### CDN — `flye.cdn`
77
+
78
+ Image hosting with public links, organized in projects and folders. Docs: https://flye.win/apis/cdn
79
+
80
+ | Method | HTTP | |
81
+ | ------ | ---- | - |
82
+ | `cdn.upload(image=open("photo.jpg", "rb"), project="...")` | `POST /cdn/upload` | Upload an image |
83
+ | `cdn.list_projects()` | `GET /cdn/projects` | List projects |
84
+ | `cdn.create_project(name="...")` | `POST /cdn/projects` | Create a project |
85
+ | `cdn.get_project("...")` | `GET /cdn/projects/{project}` | List images |
86
+ | `cdn.get_image("...")` | `GET /cdn/images/{id}` | Image details |
87
+ | `cdn.delete_image("...")` | `DELETE /cdn/images/{id}` | Delete an image |
88
+ | `cdn.delete_project("...")` | `DELETE /cdn/projects/{project}` | Delete a project |
89
+ | `cdn.get_usage()` | `GET /cdn/usage` | Storage and limits |
90
+
91
+ ### Utils — `flye.utils`
92
+
93
+ UUIDs, hashes, base64 and IP inspection in one call. Docs: https://flye.win/apis/utils
94
+
95
+ | Method | HTTP | |
96
+ | ------ | ---- | - |
97
+ | `utils.uuid()` | `GET /utils/uuid` | Generate UUIDs |
98
+ | `utils.hash(text="flye")` | `GET /utils/hash` | Compute a hash |
99
+ | `utils.base64(text="...")` | `POST /utils/base64` | Encode / decode base64 |
100
+ | `utils.ip()` | `GET /utils/ip` | Source IP |
101
+
102
+ Uploads take bytes, an open binary file, a path, or `(filename, bytes)`.
103
+
104
+ Full reference and the OpenAPI spec: https://flye.win/docs · https://flye.win/openapi.json
@@ -0,0 +1,89 @@
1
+ # flyewin
2
+
3
+ Official Python client for the [flye.win](https://flye.win) APIs. No dependencies, Python 3.8+.
4
+
5
+ ```sh
6
+ pip install flyewin
7
+ ```
8
+
9
+ ```python
10
+ from flyewin import Flye
11
+
12
+ flye = Flye() # reads FLYE_API_KEY, or Flye("flye_sk_...")
13
+
14
+ print(flye.utils.uuid(count=2))
15
+ ```
16
+
17
+ ## Test keys
18
+
19
+ Keys that start with `flye_test_` answer with the documented examples, need no plan and
20
+ use no quota (30 requests/minute, 1,000/day). Use them while building and in CI, then
21
+ switch to a `flye_sk_` key. Create both at https://flye.win/dashboard/keys.
22
+
23
+ ## Errors
24
+
25
+ Every failure raises `FlyeError` with a stable `code`:
26
+
27
+ ```python
28
+ from flyewin import Flye, FlyeError
29
+
30
+ try:
31
+ flye.utils.hash(text="hi")
32
+ except FlyeError as err:
33
+ if err.code == "rate_limited":
34
+ ... # err.status == 429
35
+ ```
36
+
37
+ Network errors and 5xx on GET requests are retried twice with backoff, and so is a `429`
38
+ with a short `Retry-After`. Change it with `Flye(max_retries=0)`.
39
+
40
+ Rate limit and quota headers of the last call are in `flye.last_response.headers`
41
+ (`x-ratelimit-remaining`, `x-quota-remaining`).
42
+
43
+ ## Options
44
+
45
+ `Flye(api_key=None, *, base_url="https://flye.win/v1", timeout=30.0, max_retries=2)`
46
+
47
+ Every method also takes `timeout=` for that call.
48
+
49
+ ## Methods
50
+
51
+ ### Stickers — `flye.stickers`
52
+
53
+ Search stickers and animated GIFs, with ready-to-use links. Docs: https://flye.win/apis/stickers
54
+
55
+ | Method | HTTP | |
56
+ | ------ | ---- | - |
57
+ | `stickers.search()` | `GET /stickers/search` | Search stickers |
58
+ | `stickers.list_packs(q="gato")` | `GET /stickers/packs` | List packs |
59
+ | `stickers.get_pack("...")` | `GET /stickers/packs/{id}` | Stickers in a pack |
60
+
61
+ ### CDN — `flye.cdn`
62
+
63
+ Image hosting with public links, organized in projects and folders. Docs: https://flye.win/apis/cdn
64
+
65
+ | Method | HTTP | |
66
+ | ------ | ---- | - |
67
+ | `cdn.upload(image=open("photo.jpg", "rb"), project="...")` | `POST /cdn/upload` | Upload an image |
68
+ | `cdn.list_projects()` | `GET /cdn/projects` | List projects |
69
+ | `cdn.create_project(name="...")` | `POST /cdn/projects` | Create a project |
70
+ | `cdn.get_project("...")` | `GET /cdn/projects/{project}` | List images |
71
+ | `cdn.get_image("...")` | `GET /cdn/images/{id}` | Image details |
72
+ | `cdn.delete_image("...")` | `DELETE /cdn/images/{id}` | Delete an image |
73
+ | `cdn.delete_project("...")` | `DELETE /cdn/projects/{project}` | Delete a project |
74
+ | `cdn.get_usage()` | `GET /cdn/usage` | Storage and limits |
75
+
76
+ ### Utils — `flye.utils`
77
+
78
+ UUIDs, hashes, base64 and IP inspection in one call. Docs: https://flye.win/apis/utils
79
+
80
+ | Method | HTTP | |
81
+ | ------ | ---- | - |
82
+ | `utils.uuid()` | `GET /utils/uuid` | Generate UUIDs |
83
+ | `utils.hash(text="flye")` | `GET /utils/hash` | Compute a hash |
84
+ | `utils.base64(text="...")` | `POST /utils/base64` | Encode / decode base64 |
85
+ | `utils.ip()` | `GET /utils/ip` | Source IP |
86
+
87
+ Uploads take bytes, an open binary file, a path, or `(filename, bytes)`.
88
+
89
+ Full reference and the OpenAPI spec: https://flye.win/docs · https://flye.win/openapi.json
@@ -0,0 +1,5 @@
1
+ """Official client for the flye.win APIs. See https://flye.win/docs."""
2
+
3
+ from ._client import DEFAULT_BASE_URL, FileInput, Flye, FlyeError, Response, __version__
4
+
5
+ __all__ = ["Flye", "FlyeError", "FileInput", "Response", "DEFAULT_BASE_URL", "__version__"]
@@ -0,0 +1,490 @@
1
+ """Official client for the flye.win APIs.
2
+
3
+ Generated from the API catalog by cmd/sdkgen. Do not edit by hand.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import mimetypes
10
+ import os
11
+ import random
12
+ import socket
13
+ import time
14
+ import urllib.error
15
+ import urllib.parse
16
+ import urllib.request
17
+ import uuid
18
+ from typing import IO, Any, Dict, List, Mapping, Optional, Tuple, Union
19
+
20
+ __version__ = "0.1.0"
21
+ DEFAULT_BASE_URL = "https://flye.win" + "/v1"
22
+
23
+ #: A file to upload: raw bytes, an open binary file, a path, or (filename, bytes[, content_type]).
24
+ FileInput = Union[bytes, bytearray, IO[bytes], "os.PathLike[str]", str, Tuple[str, bytes], Tuple[str, bytes, str]]
25
+
26
+
27
+ class FlyeError(Exception):
28
+ """Raised for every failed call. ``code`` is stable: "rate_limited", "invalid_api_key"..."""
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ *,
34
+ status: Optional[int] = None,
35
+ code: str = "error",
36
+ body: Any = None,
37
+ headers: Optional[Mapping[str, str]] = None,
38
+ ) -> None:
39
+ super().__init__(message)
40
+ self.message = message
41
+ self.status = status
42
+ self.code = code
43
+ self.body = body
44
+ self.headers = dict(headers or {})
45
+
46
+ def __repr__(self) -> str:
47
+ return f"FlyeError(status={self.status!r}, code={self.code!r}, message={self.message!r})"
48
+
49
+
50
+ class Response:
51
+ """Status and headers of the last response (X-RateLimit-*, X-Quota-*, X-Flye-Mode)."""
52
+
53
+ def __init__(self, status: int, headers: Mapping[str, str]) -> None:
54
+ self.status = status
55
+ self.headers = {k.lower(): v for k, v in headers.items()}
56
+
57
+
58
+ def _text(value: Any) -> str:
59
+ if isinstance(value, bool):
60
+ return "true" if value else "false"
61
+ return str(value)
62
+
63
+
64
+ def _file(value: Any, field: str) -> Tuple[str, bytes, str]:
65
+ if isinstance(value, tuple):
66
+ name, data = value[0], value[1]
67
+ ctype = value[2] if len(value) > 2 else None
68
+ elif isinstance(value, (bytes, bytearray)):
69
+ name, data, ctype = field, bytes(value), None
70
+ elif hasattr(value, "read"):
71
+ data = value.read()
72
+ name, ctype = os.path.basename(getattr(value, "name", field) or field), None
73
+ elif isinstance(value, (str, os.PathLike)):
74
+ path = os.fspath(value)
75
+ with open(path, "rb") as fh:
76
+ data = fh.read()
77
+ name, ctype = os.path.basename(path), None
78
+ else:
79
+ raise FlyeError("File parameters take bytes, an open binary file, a path or (filename, bytes).", code="invalid_file")
80
+ if isinstance(data, str):
81
+ data = data.encode()
82
+ ctype = ctype or mimetypes.guess_type(name)[0] or "application/octet-stream"
83
+ return name, bytes(data), ctype
84
+
85
+
86
+ def _multipart(fields: List[Tuple[str, str]], files: List[Tuple[str, Tuple[str, bytes, str]]]) -> Tuple[bytes, str]:
87
+ boundary = "flyewin-" + uuid.uuid4().hex
88
+ out = bytearray()
89
+ for name, value in fields:
90
+ out += f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n'.encode()
91
+ out += value.encode() + b"\r\n"
92
+ for name, (filename, data, ctype) in files:
93
+ safe = filename.replace('"', "")
94
+ out += f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"; filename="{safe}"\r\n'.encode()
95
+ out += f"Content-Type: {ctype}\r\n\r\n".encode()
96
+ out += data + b"\r\n"
97
+ out += f"--{boundary}--\r\n".encode()
98
+ return bytes(out), f"multipart/form-data; boundary={boundary}"
99
+
100
+
101
+ def _decode(raw: bytes, ctype: str) -> Any:
102
+ if not raw:
103
+ return None
104
+ if "json" in ctype:
105
+ try:
106
+ return json.loads(raw.decode("utf-8"))
107
+ except ValueError:
108
+ return raw.decode("utf-8", "replace")
109
+ if ctype.startswith("text/"):
110
+ return raw.decode("utf-8", "replace")
111
+ return raw
112
+
113
+
114
+ def _backoff(attempt: int) -> float:
115
+ return min(8.0, 0.5 * 2**attempt) * (0.75 + random.random() * 0.5)
116
+
117
+
118
+ class Flye:
119
+ """Client for the flye.win APIs.
120
+
121
+ >>> from flyewin import Flye
122
+ >>> flye = Flye() # reads FLYE_API_KEY
123
+ >>> flye.utils.uuid(count=2)
124
+ """
125
+
126
+ def __init__(
127
+ self,
128
+ api_key: Optional[str] = None,
129
+ *,
130
+ base_url: Optional[str] = None,
131
+ timeout: float = 30.0,
132
+ max_retries: int = 2,
133
+ ) -> None:
134
+ self.api_key = api_key or os.environ.get("FLYE_API_KEY") or ""
135
+ if not self.api_key:
136
+ raise FlyeError("Missing API key: pass Flye(api_key) or set FLYE_API_KEY.", code="missing_api_key")
137
+ self.base_url = (base_url or os.environ.get("FLYE_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
138
+ self.timeout = timeout
139
+ self.max_retries = max_retries
140
+ #: Status and headers of the last response.
141
+ self.last_response: Optional[Response] = None
142
+ #: Stickers — Search stickers and animated GIFs, with ready-to-use links.
143
+ self.stickers = _Stickers(self)
144
+ #: CDN — Image hosting with public links, organized in projects and folders.
145
+ self.cdn = _Cdn(self)
146
+ #: Utils — UUIDs, hashes, base64 and IP inspection in one call.
147
+ self.utils = _Utils(self)
148
+
149
+ def request(
150
+ self,
151
+ method: str,
152
+ path: str,
153
+ params: List[Tuple[str, str, Any]],
154
+ extra: Optional[Dict[str, Any]] = None,
155
+ *,
156
+ default: str = "query",
157
+ timeout: Optional[float] = None,
158
+ ) -> Any:
159
+ """Low-level call used by every method. ``params`` is a list of (name, location, value)."""
160
+ query: List[Tuple[str, str]] = []
161
+ headers = {
162
+ "Authorization": f"Bearer {self.api_key}",
163
+ "Accept": "application/json",
164
+ "User-Agent": f"flyewin-python/{__version__}",
165
+ }
166
+ body_json: Dict[str, Any] = {}
167
+ fields: List[Tuple[str, str]] = []
168
+ files: List[Tuple[str, Tuple[str, bytes, str]]] = []
169
+ items = list(params) + [(k, default, v) for k, v in (extra or {}).items()]
170
+ for name, where, value in items:
171
+ if value is None:
172
+ continue
173
+ if where == "path":
174
+ path = path.replace("{" + name + "}", urllib.parse.quote(_text(value), safe=""))
175
+ elif where == "query":
176
+ for item in value if isinstance(value, (list, tuple)) else [value]:
177
+ query.append((name, _text(item)))
178
+ elif where == "header":
179
+ headers[name] = _text(value)
180
+ elif where == "file":
181
+ files.append((name, _file(value, name)))
182
+ elif where == "form":
183
+ if isinstance(value, (bytes, bytearray, tuple)) or hasattr(value, "read"):
184
+ files.append((name, _file(value, name)))
185
+ else:
186
+ fields.append((name, _text(value)))
187
+ else:
188
+ body_json[name] = value
189
+ if "{" in path:
190
+ raise FlyeError(f"Missing path parameter in {method} {path}.", code="missing_param")
191
+
192
+ data: Optional[bytes] = None
193
+ if files or fields:
194
+ data, headers["Content-Type"] = _multipart(fields, files)
195
+ elif body_json or default == "body":
196
+ data = json.dumps(body_json).encode("utf-8")
197
+ headers["Content-Type"] = "application/json"
198
+ url = self.base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
199
+ idempotent = method == "GET"
200
+
201
+ attempt = 0
202
+ while True:
203
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
204
+ try:
205
+ with urllib.request.urlopen(req, timeout=timeout or self.timeout) as resp:
206
+ self.last_response = Response(resp.status, resp.headers)
207
+ return _decode(resp.read(), resp.headers.get("Content-Type", ""))
208
+ except urllib.error.HTTPError as err:
209
+ raw = err.read()
210
+ self.last_response = Response(err.code, err.headers)
211
+ payload = _decode(raw, err.headers.get("Content-Type", ""))
212
+ retry_after = 0.0
213
+ try:
214
+ retry_after = float(err.headers.get("Retry-After") or 0)
215
+ except ValueError:
216
+ pass
217
+ # 429 with a short Retry-After (per-minute limit) and 5xx on reads are worth another try.
218
+ can_retry = (err.code == 429 and 0 < retry_after <= 60) or (idempotent and err.code >= 500)
219
+ if can_retry and attempt < self.max_retries:
220
+ time.sleep(retry_after if err.code == 429 else _backoff(attempt))
221
+ attempt += 1
222
+ continue
223
+ info = payload.get("error") if isinstance(payload, dict) else None
224
+ info = info if isinstance(info, dict) else {}
225
+ raise FlyeError(
226
+ info.get("message") or f"HTTP {err.code}",
227
+ status=err.code,
228
+ code=info.get("code") or "http_error",
229
+ body=payload,
230
+ headers=err.headers,
231
+ ) from None
232
+ except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError) as err:
233
+ if idempotent and attempt < self.max_retries:
234
+ time.sleep(_backoff(attempt))
235
+ attempt += 1
236
+ continue
237
+ reason = getattr(err, "reason", err)
238
+ timed_out = isinstance(reason, (socket.timeout, TimeoutError))
239
+ raise FlyeError(
240
+ "Request timed out." if timed_out else f"Network error: {reason}",
241
+ code="timeout" if timed_out else "network_error",
242
+ ) from err
243
+
244
+
245
+ class _Stickers:
246
+ """Stickers — Search stickers and animated GIFs, with ready-to-use links.
247
+
248
+ Docs: https://flye.win/apis/stickers
249
+ """
250
+
251
+ def __init__(self, client: Flye) -> None:
252
+ self._client = client
253
+
254
+ def search(self, *, q: Optional[str] = None, end_user_id: Optional[str] = None, max_wait_ms: Optional[int] = None, limit: Optional[int] = None, type: Optional[str] = None, page: Optional[int] = None, rotate: Optional[bool] = None, timeout: Optional[float] = None, **extra: Any) -> Any:
255
+ """Search stickers
256
+
257
+ Returns the most relevant pack for the term, with a link for each sticker. With rotate=true, each call returns the next pack, so bots don't repeat the same result. A term never searched before may come right away with partial=true, with what we already have, while the full search finishes.
258
+
259
+ Args:
260
+ q: Search term, 2 to 80 characters. Without it, the response has 30 featured stickers, drawn on every call, with trending=true. Example: gato
261
+ end_user_id: Scale plan: identifies the user on your platform, so the limit applies per user and not to the account. Use a stable id, preferably a hash.
262
+ max_wait_ms: Longest wait for a term never searched before, 0 to 15000 ms. Past that, the response returns right away with pending=true and the search continues: repeat the call shortly. Without it, waits for the result. Example: 300
263
+ limit: Stickers per response, 1 to 30. Defaults to 30. Example: 10
264
+ type: all, animated or static. Defaults to all.
265
+ page: Pack position in the ranking, starting at 1. Defaults to 1.
266
+ rotate: true returns the next pack on each call from this account. Ignores page.
267
+
268
+ Docs: https://flye.win/apis/stickers
269
+ """
270
+ return self._client.request(
271
+ "GET", "/stickers/search", [("q", "query", q), ("X-End-User-Id", "header", end_user_id), ("max_wait_ms", "query", max_wait_ms), ("limit", "query", limit), ("type", "query", type), ("page", "query", page), ("rotate", "query", rotate)], extra, default="query", timeout=timeout
272
+ )
273
+
274
+ def list_packs(self, *, q: str, end_user_id: Optional[str] = None, max_wait_ms: Optional[int] = None, limit: Optional[int] = None, page: Optional[int] = None, type: Optional[str] = None, timeout: Optional[float] = None, **extra: Any) -> Any:
275
+ """List packs
276
+
277
+ Lists the packs found for the term, with a cover sticker, so you can build a picker.
278
+
279
+ Args:
280
+ q: Search term, 2 to 80 characters. Example: gato
281
+ end_user_id: Scale plan: identifies the user on your platform, so the limit applies per user and not to the account. Use a stable id, preferably a hash.
282
+ max_wait_ms: Longest wait for a term never searched before, 0 to 15000 ms. Past that, the response returns right away with pending=true and the search continues: repeat the call shortly. Without it, waits for the result. Example: 300
283
+ limit: Packs per page, 1 to 20. Defaults to 10. Example: 5
284
+ page: Page, starting at 1.
285
+ type: all, animated or static. Defaults to all.
286
+
287
+ Docs: https://flye.win/apis/stickers
288
+ """
289
+ return self._client.request(
290
+ "GET", "/stickers/packs", [("q", "query", q), ("X-End-User-Id", "header", end_user_id), ("max_wait_ms", "query", max_wait_ms), ("limit", "query", limit), ("page", "query", page), ("type", "query", type)], extra, default="query", timeout=timeout
291
+ )
292
+
293
+ def get_pack(self, id: str, *, timeout: Optional[float] = None, **extra: Any) -> Any:
294
+ """Stickers in a pack
295
+
296
+ Returns every sticker in a pack by its id.
297
+
298
+ Args:
299
+ id: Pack id, from /search or /packs.
300
+
301
+ Docs: https://flye.win/apis/stickers
302
+ """
303
+ return self._client.request(
304
+ "GET", "/stickers/packs/{id}", [("id", "path", id)], extra, default="query", timeout=timeout
305
+ )
306
+
307
+ class _Cdn:
308
+ """CDN — Image hosting with public links, organized in projects and folders.
309
+
310
+ Docs: https://flye.win/apis/cdn
311
+ """
312
+
313
+ def __init__(self, client: Flye) -> None:
314
+ self._client = client
315
+
316
+ def upload(self, *, image: FileInput, project: str, folder: Optional[str] = None, timeout: Optional[float] = None, **extra: Any) -> Any:
317
+ """Upload an image
318
+
319
+ Uploads one image as multipart/form-data. The project is created the first time you use it. Accepts jpg, jpeg, png, gif, webp, svg, up to your plan's per-file limit. The type is checked from the file's content, and SVG with scripts or external links is rejected.
320
+
321
+ Args:
322
+ image: The file. Also accepted under the name file.
323
+ project: Project name: letters, numbers, - and _, up to 48 characters. Uppercase becomes lowercase.
324
+ folder: Folder inside the project, optional, with the same rules as the name.
325
+
326
+ Docs: https://flye.win/apis/cdn
327
+ """
328
+ return self._client.request(
329
+ "POST", "/cdn/upload", [("image", "file", image), ("project", "form", project), ("folder", "form", folder)], extra, default="form", timeout=timeout
330
+ )
331
+
332
+ def list_projects(self, *, timeout: Optional[float] = None, **extra: Any) -> Any:
333
+ """List projects
334
+
335
+ Lists your account's projects, with the image count and storage used by each.
336
+
337
+ Docs: https://flye.win/apis/cdn
338
+ """
339
+ return self._client.request(
340
+ "GET", "/cdn/projects", [], extra, default="query", timeout=timeout
341
+ )
342
+
343
+ def create_project(self, *, name: str, timeout: Optional[float] = None, **extra: Any) -> Any:
344
+ """Create a project
345
+
346
+ Creates an empty project. Optional: uploading already creates the project when it doesn't exist.
347
+
348
+ Args:
349
+ name: Project name.
350
+
351
+ Docs: https://flye.win/apis/cdn
352
+ """
353
+ return self._client.request(
354
+ "POST", "/cdn/projects", [("name", "body", name)], extra, default="body", timeout=timeout
355
+ )
356
+
357
+ def get_project(self, project: str, *, folder: Optional[str] = None, page: Optional[int] = None, per_page: Optional[int] = None, timeout: Optional[float] = None, **extra: Any) -> Any:
358
+ """List images
359
+
360
+ Lists the project's images, newest first, and the folders it has.
361
+
362
+ Args:
363
+ project: Project name.
364
+ folder: Only this folder's images. Without it, every image in the project. Use / for only those outside folders.
365
+ page: Page, starting at 1.
366
+ per_page: Items per page, 1 to 100. Defaults to 20.
367
+
368
+ Docs: https://flye.win/apis/cdn
369
+ """
370
+ return self._client.request(
371
+ "GET", "/cdn/projects/{project}", [("project", "path", project), ("folder", "query", folder), ("page", "query", page), ("per_page", "query", per_page)], extra, default="query", timeout=timeout
372
+ )
373
+
374
+ def get_image(self, id: str, *, timeout: Optional[float] = None, **extra: Any) -> Any:
375
+ """Image details
376
+
377
+ Returns one of your images by the id returned on upload.
378
+
379
+ Args:
380
+ id: Image id.
381
+
382
+ Docs: https://flye.win/apis/cdn
383
+ """
384
+ return self._client.request(
385
+ "GET", "/cdn/images/{id}", [("id", "path", id)], extra, default="query", timeout=timeout
386
+ )
387
+
388
+ def delete_image(self, id: str, *, timeout: Optional[float] = None, **extra: Any) -> Any:
389
+ """Delete an image
390
+
391
+ Deletes the image and frees the space right away. The link stops working within moments.
392
+
393
+ Args:
394
+ id: Image id.
395
+
396
+ Docs: https://flye.win/apis/cdn
397
+ """
398
+ return self._client.request(
399
+ "DELETE", "/cdn/images/{id}", [("id", "path", id)], extra, default="query", timeout=timeout
400
+ )
401
+
402
+ def delete_project(self, project: str, *, timeout: Optional[float] = None, **extra: Any) -> Any:
403
+ """Delete a project
404
+
405
+ Deletes the project with all its images and frees the space right away. This can't be undone.
406
+
407
+ Args:
408
+ project: Project name.
409
+
410
+ Docs: https://flye.win/apis/cdn
411
+ """
412
+ return self._client.request(
413
+ "DELETE", "/cdn/projects/{project}", [("project", "path", project)], extra, default="query", timeout=timeout
414
+ )
415
+
416
+ def get_usage(self, *, timeout: Optional[float] = None, **extra: Any) -> Any:
417
+ """Storage and limits
418
+
419
+ Shows the storage used, your plan's limits and the largest file accepted. A null limit means unlimited.
420
+
421
+ Docs: https://flye.win/apis/cdn
422
+ """
423
+ return self._client.request(
424
+ "GET", "/cdn/usage", [], extra, default="query", timeout=timeout
425
+ )
426
+
427
+ class _Utils:
428
+ """Utils — UUIDs, hashes, base64 and IP inspection in one call.
429
+
430
+ Docs: https://flye.win/apis/utils
431
+ """
432
+
433
+ def __init__(self, client: Flye) -> None:
434
+ self._client = client
435
+
436
+ def uuid(self, *, count: Optional[int] = None, version: Optional[int] = None, timeout: Optional[float] = None, **extra: Any) -> Any:
437
+ """Generate UUIDs
438
+
439
+ Generates 1 to 100 UUIDs. Version 7 sorts by time, which suits primary keys.
440
+
441
+ Args:
442
+ count: How many, from 1 to 100. Defaults to 1. Example: 2
443
+ version: 4 (random) or 7 (time-ordered). Defaults to 4. Example: 7
444
+
445
+ Docs: https://flye.win/apis/utils
446
+ """
447
+ return self._client.request(
448
+ "GET", "/utils/uuid", [("count", "query", count), ("version", "query", version)], extra, default="query", timeout=timeout
449
+ )
450
+
451
+ def hash(self, *, text: str, algo: Optional[str] = None, timeout: Optional[float] = None, **extra: Any) -> Any:
452
+ """Compute a hash
453
+
454
+ Hashes the given text and returns it as hex.
455
+
456
+ Args:
457
+ text: Input text (up to 64 KB). Example: flye
458
+ algo: md5, sha1, sha256 or sha512. Defaults to sha256. Example: sha256
459
+
460
+ Docs: https://flye.win/apis/utils
461
+ """
462
+ return self._client.request(
463
+ "GET", "/utils/hash", [("text", "query", text), ("algo", "query", algo)], extra, default="query", timeout=timeout
464
+ )
465
+
466
+ def base64(self, *, text: str, mode: Optional[str] = None, timeout: Optional[float] = None, **extra: Any) -> Any:
467
+ """Encode / decode base64
468
+
469
+ Converts text to and from standard base64 (RFC 4648).
470
+
471
+ Args:
472
+ text: Content to convert.
473
+ mode: encode or decode. Defaults to encode.
474
+
475
+ Docs: https://flye.win/apis/utils
476
+ """
477
+ return self._client.request(
478
+ "POST", "/utils/base64", [("text", "body", text), ("mode", "body", mode)], extra, default="body", timeout=timeout
479
+ )
480
+
481
+ def ip(self, *, timeout: Optional[float] = None, **extra: Any) -> Any:
482
+ """Source IP
483
+
484
+ Returns the IP and user agent your request reached the platform with.
485
+
486
+ Docs: https://flye.win/apis/utils
487
+ """
488
+ return self._client.request(
489
+ "GET", "/utils/ip", [], extra, default="query", timeout=timeout
490
+ )
File without changes
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.1
2
+ Name: flyewin
3
+ Version: 0.1.0
4
+ Summary: Official client for the flye.win APIs. No dependencies.
5
+ License: MIT
6
+ Project-URL: Homepage, https://flye.win
7
+ Project-URL: Documentation, https://flye.win/docs
8
+ Keywords: flye,flye.win,api,sdk
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+
16
+ # flyewin
17
+
18
+ Official Python client for the [flye.win](https://flye.win) APIs. No dependencies, Python 3.8+.
19
+
20
+ ```sh
21
+ pip install flyewin
22
+ ```
23
+
24
+ ```python
25
+ from flyewin import Flye
26
+
27
+ flye = Flye() # reads FLYE_API_KEY, or Flye("flye_sk_...")
28
+
29
+ print(flye.utils.uuid(count=2))
30
+ ```
31
+
32
+ ## Test keys
33
+
34
+ Keys that start with `flye_test_` answer with the documented examples, need no plan and
35
+ use no quota (30 requests/minute, 1,000/day). Use them while building and in CI, then
36
+ switch to a `flye_sk_` key. Create both at https://flye.win/dashboard/keys.
37
+
38
+ ## Errors
39
+
40
+ Every failure raises `FlyeError` with a stable `code`:
41
+
42
+ ```python
43
+ from flyewin import Flye, FlyeError
44
+
45
+ try:
46
+ flye.utils.hash(text="hi")
47
+ except FlyeError as err:
48
+ if err.code == "rate_limited":
49
+ ... # err.status == 429
50
+ ```
51
+
52
+ Network errors and 5xx on GET requests are retried twice with backoff, and so is a `429`
53
+ with a short `Retry-After`. Change it with `Flye(max_retries=0)`.
54
+
55
+ Rate limit and quota headers of the last call are in `flye.last_response.headers`
56
+ (`x-ratelimit-remaining`, `x-quota-remaining`).
57
+
58
+ ## Options
59
+
60
+ `Flye(api_key=None, *, base_url="https://flye.win/v1", timeout=30.0, max_retries=2)`
61
+
62
+ Every method also takes `timeout=` for that call.
63
+
64
+ ## Methods
65
+
66
+ ### Stickers — `flye.stickers`
67
+
68
+ Search stickers and animated GIFs, with ready-to-use links. Docs: https://flye.win/apis/stickers
69
+
70
+ | Method | HTTP | |
71
+ | ------ | ---- | - |
72
+ | `stickers.search()` | `GET /stickers/search` | Search stickers |
73
+ | `stickers.list_packs(q="gato")` | `GET /stickers/packs` | List packs |
74
+ | `stickers.get_pack("...")` | `GET /stickers/packs/{id}` | Stickers in a pack |
75
+
76
+ ### CDN — `flye.cdn`
77
+
78
+ Image hosting with public links, organized in projects and folders. Docs: https://flye.win/apis/cdn
79
+
80
+ | Method | HTTP | |
81
+ | ------ | ---- | - |
82
+ | `cdn.upload(image=open("photo.jpg", "rb"), project="...")` | `POST /cdn/upload` | Upload an image |
83
+ | `cdn.list_projects()` | `GET /cdn/projects` | List projects |
84
+ | `cdn.create_project(name="...")` | `POST /cdn/projects` | Create a project |
85
+ | `cdn.get_project("...")` | `GET /cdn/projects/{project}` | List images |
86
+ | `cdn.get_image("...")` | `GET /cdn/images/{id}` | Image details |
87
+ | `cdn.delete_image("...")` | `DELETE /cdn/images/{id}` | Delete an image |
88
+ | `cdn.delete_project("...")` | `DELETE /cdn/projects/{project}` | Delete a project |
89
+ | `cdn.get_usage()` | `GET /cdn/usage` | Storage and limits |
90
+
91
+ ### Utils — `flye.utils`
92
+
93
+ UUIDs, hashes, base64 and IP inspection in one call. Docs: https://flye.win/apis/utils
94
+
95
+ | Method | HTTP | |
96
+ | ------ | ---- | - |
97
+ | `utils.uuid()` | `GET /utils/uuid` | Generate UUIDs |
98
+ | `utils.hash(text="flye")` | `GET /utils/hash` | Compute a hash |
99
+ | `utils.base64(text="...")` | `POST /utils/base64` | Encode / decode base64 |
100
+ | `utils.ip()` | `GET /utils/ip` | Source IP |
101
+
102
+ Uploads take bytes, an open binary file, a path, or `(filename, bytes)`.
103
+
104
+ Full reference and the OpenAPI spec: https://flye.win/docs · https://flye.win/openapi.json
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ flyewin/__init__.py
4
+ flyewin/_client.py
5
+ flyewin/py.typed
6
+ flyewin.egg-info/PKG-INFO
7
+ flyewin.egg-info/SOURCES.txt
8
+ flyewin.egg-info/dependency_links.txt
9
+ flyewin.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ flyewin
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flyewin"
7
+ version = "0.1.0"
8
+ description = "Official client for the flye.win APIs. No dependencies."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ keywords = ["flye", "flye.win", "api", "sdk"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Typing :: Typed",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://flye.win"
22
+ Documentation = "https://flye.win/docs"
23
+
24
+ [tool.setuptools]
25
+ packages = ["flyewin"]
26
+
27
+ [tool.setuptools.package-data]
28
+ flyewin = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+