unixsr 0.0.1__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.
- unixsr-0.0.1/LICENSE +19 -0
- unixsr-0.0.1/PKG-INFO +72 -0
- unixsr-0.0.1/README.md +53 -0
- unixsr-0.0.1/pyproject.toml +28 -0
- unixsr-0.0.1/setup.cfg +4 -0
- unixsr-0.0.1/unixsr/__init__.py +67 -0
- unixsr-0.0.1/unixsr/_adapter.py +104 -0
- unixsr-0.0.1/unixsr/_async.py +180 -0
- unixsr-0.0.1/unixsr/_client.py +136 -0
- unixsr-0.0.1/unixsr/_config.py +75 -0
- unixsr-0.0.1/unixsr/_error.py +107 -0
- unixsr-0.0.1/unixsr/_flags.py +217 -0
- unixsr-0.0.1/unixsr/_pipe.py +146 -0
- unixsr-0.0.1/unixsr/_request.py +37 -0
- unixsr-0.0.1/unixsr/py.typed +0 -0
- unixsr-0.0.1/unixsr.egg-info/PKG-INFO +72 -0
- unixsr-0.0.1/unixsr.egg-info/SOURCES.txt +28 -0
- unixsr-0.0.1/unixsr.egg-info/dependency_links.txt +1 -0
- unixsr-0.0.1/unixsr.egg-info/requires.txt +1 -0
- unixsr-0.0.1/unixsr.egg-info/top_level.txt +2 -0
- unixsr-0.0.1/usr/__init__.py +67 -0
- unixsr-0.0.1/usr/_adapter.py +104 -0
- unixsr-0.0.1/usr/_async.py +180 -0
- unixsr-0.0.1/usr/_client.py +136 -0
- unixsr-0.0.1/usr/_config.py +75 -0
- unixsr-0.0.1/usr/_error.py +107 -0
- unixsr-0.0.1/usr/_flags.py +217 -0
- unixsr-0.0.1/usr/_pipe.py +146 -0
- unixsr-0.0.1/usr/_request.py +37 -0
- unixsr-0.0.1/usr/py.typed +0 -0
unixsr-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
unixsr-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: unixsr
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: UNIXSR: Unix Style Request — Axios with a Unix vocabulary HTTP client
|
|
5
|
+
Author: Unix Style Request Project
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: http,client,axios,unix,unixsr,usr
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx<1,>=0.24
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# usr
|
|
21
|
+
|
|
22
|
+
USR: Unix Style Request — Axios with a Unix vocabulary HTTP client.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
pip install usr
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from usr import *
|
|
34
|
+
|
|
35
|
+
res = cat("https://api.site.com/users")
|
|
36
|
+
print(res.status, res.data)
|
|
37
|
+
|
|
38
|
+
touch("https://api.site.com/users", {"name": "Ada"})
|
|
39
|
+
sed("https://api.site.com/users/42", {"active": False})
|
|
40
|
+
rm("https://api.site.com/users/42")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Flags
|
|
44
|
+
|
|
45
|
+
Structured keyword form or a curl-like command-line string:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
cat("https://api.site.com/me", H="Authorization: Bearer TOKEN", m=5)
|
|
49
|
+
# or
|
|
50
|
+
cat("-H 'Authorization: Bearer TOKEN' -m 5 https://api.site.com/me")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Pipes (real ``|`` operator)
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
names = cat("https://api.site.com/users") | grep(active=True) | cut("name") | head(5)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Scoped client
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
api = cd("https://api.site.com", H="Authorization: Bearer TOKEN")
|
|
63
|
+
me = api.cat("/me")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Async
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from usr import aio
|
|
70
|
+
|
|
71
|
+
res = await aio.cat("https://api.site.com/users")
|
|
72
|
+
```
|
unixsr-0.0.1/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# usr
|
|
2
|
+
|
|
3
|
+
USR: Unix Style Request — Axios with a Unix vocabulary HTTP client.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pip install usr
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from usr import *
|
|
15
|
+
|
|
16
|
+
res = cat("https://api.site.com/users")
|
|
17
|
+
print(res.status, res.data)
|
|
18
|
+
|
|
19
|
+
touch("https://api.site.com/users", {"name": "Ada"})
|
|
20
|
+
sed("https://api.site.com/users/42", {"active": False})
|
|
21
|
+
rm("https://api.site.com/users/42")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Flags
|
|
25
|
+
|
|
26
|
+
Structured keyword form or a curl-like command-line string:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
cat("https://api.site.com/me", H="Authorization: Bearer TOKEN", m=5)
|
|
30
|
+
# or
|
|
31
|
+
cat("-H 'Authorization: Bearer TOKEN' -m 5 https://api.site.com/me")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Pipes (real ``|`` operator)
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
names = cat("https://api.site.com/users") | grep(active=True) | cut("name") | head(5)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Scoped client
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
api = cd("https://api.site.com", H="Authorization: Bearer TOKEN")
|
|
44
|
+
me = api.cat("/me")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Async
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from usr import aio
|
|
51
|
+
|
|
52
|
+
res = await aio.cat("https://api.site.com/users")
|
|
53
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "unixsr"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "UNIXSR: Unix Style Request — Axios with a Unix vocabulary HTTP client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Unix Style Request Project"}]
|
|
13
|
+
keywords = ["http", "client", "axios", "unix", "unixsr", "usr"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["httpx>=0.24,<1"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["unixsr*", "usr*"]
|
unixsr-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""USR: Unix Style Request — Axios with a Unix vocabulary HTTP client.
|
|
2
|
+
|
|
3
|
+
Synchronous by default. Use ``from usr import *`` safely (explicit ``__all__``;
|
|
4
|
+
no Python builtins are shadowed).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._client import Response, UsrClient, cd, defaults, create_usr
|
|
10
|
+
from ._error import UsrError
|
|
11
|
+
from ._flags import normalize_flags, parse_flags_string, parse_flag_string
|
|
12
|
+
from ._pipe import PipeError, cut, grep, head, pipeable, sort, tail, tee, uniq, wc, xargs
|
|
13
|
+
from . import _async as aio
|
|
14
|
+
|
|
15
|
+
_client: UsrClient = create_usr()
|
|
16
|
+
|
|
17
|
+
cat = _client.cat
|
|
18
|
+
touch = _client.touch
|
|
19
|
+
mv = _client.mv
|
|
20
|
+
sed = _client.sed
|
|
21
|
+
rm = _client.rm
|
|
22
|
+
stat = _client.stat
|
|
23
|
+
man = _client.man
|
|
24
|
+
trap = _client.trap
|
|
25
|
+
untrap = _client.untrap
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def request(config: dict) -> Response:
|
|
29
|
+
return _client.request(config)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"cat",
|
|
34
|
+
"touch",
|
|
35
|
+
"mv",
|
|
36
|
+
"sed",
|
|
37
|
+
"rm",
|
|
38
|
+
"stat",
|
|
39
|
+
"man",
|
|
40
|
+
"request",
|
|
41
|
+
"trap",
|
|
42
|
+
"untrap",
|
|
43
|
+
"grep",
|
|
44
|
+
"head",
|
|
45
|
+
"tail",
|
|
46
|
+
"sort",
|
|
47
|
+
"uniq",
|
|
48
|
+
"cut",
|
|
49
|
+
"wc",
|
|
50
|
+
"tee",
|
|
51
|
+
"xargs",
|
|
52
|
+
"cd",
|
|
53
|
+
"defaults",
|
|
54
|
+
"UsrError",
|
|
55
|
+
"parse_flags_string",
|
|
56
|
+
"parse_flag_string",
|
|
57
|
+
"normalize_flags",
|
|
58
|
+
"pipeable",
|
|
59
|
+
"PipeError",
|
|
60
|
+
"Response",
|
|
61
|
+
"AsyncResponse",
|
|
62
|
+
"aio",
|
|
63
|
+
"create_usr",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
# Re-export async response type name for convenience.
|
|
67
|
+
from ._async import AsyncResponse # noqa: E402
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""HTTP transport adapter backed by httpx (synchronous)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._config import append_query, normalize_headers, resolve_url, serialize_body, validate_status_default
|
|
10
|
+
from ._error import (
|
|
11
|
+
UsrError,
|
|
12
|
+
bad_request_error,
|
|
13
|
+
bad_response_error,
|
|
14
|
+
canceled_error,
|
|
15
|
+
invalid_url_error,
|
|
16
|
+
network_error,
|
|
17
|
+
timeout_error,
|
|
18
|
+
too_many_redirects_error,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
REDIRECT_STATUS = {301, 302, 303, 307, 308}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _headers_obj(headers: httpx.Headers) -> dict:
|
|
25
|
+
return dict(headers)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def dispatch(config: dict) -> dict:
|
|
29
|
+
url = append_query(resolve_url(config.get("base_url"), str(config["url"])), config.get("params"))
|
|
30
|
+
headers = normalize_headers({"accept": "application/json", **(config.get("headers") or {})})
|
|
31
|
+
body, headers = serialize_body(config.get("data"), headers)
|
|
32
|
+
|
|
33
|
+
timeout = config.get("timeout", 0)
|
|
34
|
+
httpx_timeout = httpx.Timeout(timeout) if timeout else None
|
|
35
|
+
|
|
36
|
+
max_redirects = config.get("max_redirects")
|
|
37
|
+
follow = max_redirects is None
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
with httpx.Client(timeout=httpx_timeout, follow_redirects=follow, max_redirects=max_redirects or 20) as client:
|
|
41
|
+
response = client.request(config["method"], url, headers=headers, content=body)
|
|
42
|
+
except httpx.TimeoutException as exc:
|
|
43
|
+
raise timeout_error(config) from exc
|
|
44
|
+
except httpx.NetworkError as exc:
|
|
45
|
+
raise network_error(str(exc), config) from exc
|
|
46
|
+
except httpx.TooManyRedirects as exc:
|
|
47
|
+
raise too_many_redirects_error(config) from exc
|
|
48
|
+
except httpx.InvalidURL as exc:
|
|
49
|
+
raise invalid_url_error(str(exc), config) from exc
|
|
50
|
+
except httpx.RequestError as exc:
|
|
51
|
+
raise network_error(str(exc), config) from exc
|
|
52
|
+
|
|
53
|
+
if not follow and response.status_code in REDIRECT_STATUS:
|
|
54
|
+
raise too_many_redirects_error(
|
|
55
|
+
config,
|
|
56
|
+
response=_response_dict(response, config),
|
|
57
|
+
status=response.status_code,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
status = response.status_code
|
|
61
|
+
validate_status = config.get("validate_status") or validate_status_default
|
|
62
|
+
content_type = response.headers.get("content-type", "")
|
|
63
|
+
is_json = "application/json" in content_type
|
|
64
|
+
text = response.text
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
data = _json_loads(text) if is_json else text
|
|
68
|
+
except Exception:
|
|
69
|
+
data = text
|
|
70
|
+
|
|
71
|
+
res = {
|
|
72
|
+
"data": data,
|
|
73
|
+
"status": status,
|
|
74
|
+
"statusText": response.reason_phrase,
|
|
75
|
+
"headers": _headers_obj(response.headers),
|
|
76
|
+
"config": config,
|
|
77
|
+
"request": response,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if not validate_status(status):
|
|
81
|
+
raise bad_request_error(f"Request failed with status {status}", config, res, status=status)
|
|
82
|
+
|
|
83
|
+
return res
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _json_loads(text: str):
|
|
87
|
+
import json as _json
|
|
88
|
+
|
|
89
|
+
return _json.loads(text)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _response_dict(response: httpx.Response, config: dict) -> dict:
|
|
93
|
+
try:
|
|
94
|
+
data = response.json()
|
|
95
|
+
except Exception:
|
|
96
|
+
data = response.text
|
|
97
|
+
return {
|
|
98
|
+
"data": data,
|
|
99
|
+
"status": response.status_code,
|
|
100
|
+
"statusText": response.reason_phrase,
|
|
101
|
+
"headers": _headers_obj(response.headers),
|
|
102
|
+
"config": config,
|
|
103
|
+
"request": response,
|
|
104
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Async companion namespace for USR (httpx.AsyncClient)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._config import append_query, normalize_headers, resolve_url, serialize_body, validate_status_default
|
|
10
|
+
from ._error import (
|
|
11
|
+
UsrError,
|
|
12
|
+
bad_request_error,
|
|
13
|
+
canceled_error,
|
|
14
|
+
invalid_url_error,
|
|
15
|
+
network_error,
|
|
16
|
+
timeout_error,
|
|
17
|
+
too_many_redirects_error,
|
|
18
|
+
)
|
|
19
|
+
from ._pipe import PipeError, Pipeable
|
|
20
|
+
from ._request import NO_BODY_METHODS, METHOD_BY_COMMAND
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncResponse:
|
|
24
|
+
__slots__ = ("_data", "status", "status_text", "headers", "config", "request")
|
|
25
|
+
|
|
26
|
+
def __init__(self, raw: dict) -> None:
|
|
27
|
+
self._data = raw["data"]
|
|
28
|
+
self.status = raw["status"]
|
|
29
|
+
self.status_text = raw.get("statusText", "")
|
|
30
|
+
self.headers = raw.get("headers", {})
|
|
31
|
+
self.config = raw.get("config", {})
|
|
32
|
+
self.request = raw.get("request")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def data(self) -> Any:
|
|
36
|
+
return self._data
|
|
37
|
+
|
|
38
|
+
def __or__(self, stage):
|
|
39
|
+
return Pipeable(stage(self._data))
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str: # pragma: no cover - debug aid
|
|
42
|
+
return f"AsyncResponse(status={self.status}, data={self._data!r})"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _adispatch(config: dict) -> dict:
|
|
46
|
+
url = append_query(resolve_url(config.get("base_url"), str(config["url"])), config.get("params"))
|
|
47
|
+
headers = normalize_headers({"accept": "application/json", **(config.get("headers") or {})})
|
|
48
|
+
body, headers = serialize_body(config.get("data"), headers)
|
|
49
|
+
|
|
50
|
+
timeout = config.get("timeout", 0)
|
|
51
|
+
httpx_timeout = httpx.Timeout(timeout) if timeout else None
|
|
52
|
+
max_redirects = config.get("max_redirects")
|
|
53
|
+
follow = max_redirects is None
|
|
54
|
+
|
|
55
|
+
async with httpx.AsyncClient(timeout=httpx_timeout, follow_redirects=follow, max_redirects=max_redirects or 20) as client:
|
|
56
|
+
try:
|
|
57
|
+
response = await client.request(config["method"], url, headers=headers, content=body)
|
|
58
|
+
except httpx.TimeoutException as exc:
|
|
59
|
+
raise timeout_error(config) from exc
|
|
60
|
+
except httpx.NetworkError as exc:
|
|
61
|
+
raise network_error(str(exc), config) from exc
|
|
62
|
+
except httpx.TooManyRedirects as exc:
|
|
63
|
+
raise too_many_redirects_error(config) from exc
|
|
64
|
+
except httpx.InvalidURL as exc:
|
|
65
|
+
raise invalid_url_error(str(exc), config) from exc
|
|
66
|
+
except httpx.RequestError as exc:
|
|
67
|
+
raise network_error(str(exc), config) from exc
|
|
68
|
+
|
|
69
|
+
status = response.status_code
|
|
70
|
+
validate_status = config.get("validate_status") or validate_status_default
|
|
71
|
+
content_type = response.headers.get("content-type", "")
|
|
72
|
+
is_json = "application/json" in content_type
|
|
73
|
+
text = response.text
|
|
74
|
+
try:
|
|
75
|
+
import json as _json
|
|
76
|
+
|
|
77
|
+
data = _json.loads(text) if is_json else text
|
|
78
|
+
except Exception:
|
|
79
|
+
data = text
|
|
80
|
+
|
|
81
|
+
res = {
|
|
82
|
+
"data": data,
|
|
83
|
+
"status": status,
|
|
84
|
+
"statusText": response.reason_phrase,
|
|
85
|
+
"headers": dict(response.headers),
|
|
86
|
+
"config": config,
|
|
87
|
+
"request": response,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if not validate_status(status):
|
|
91
|
+
raise bad_request_error(f"Request failed with status {status}", config, res, status=status)
|
|
92
|
+
return res
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class AsyncUsrClient:
|
|
96
|
+
def __init__(self, base_config: Optional[dict] = None) -> None:
|
|
97
|
+
self.defaults: dict = dict(base_config or {})
|
|
98
|
+
self._traps: list = []
|
|
99
|
+
|
|
100
|
+
async def cat(self, url: str, options: Optional[dict] = None) -> AsyncResponse:
|
|
101
|
+
return await self._call("cat", url, None, options)
|
|
102
|
+
|
|
103
|
+
async def touch(self, url: str, data: Any = None, options: Optional[dict] = None) -> AsyncResponse:
|
|
104
|
+
return await self._call("touch", url, data, options)
|
|
105
|
+
|
|
106
|
+
async def mv(self, url: str, data: Any = None, options: Optional[dict] = None) -> AsyncResponse:
|
|
107
|
+
return await self._call("mv", url, data, options)
|
|
108
|
+
|
|
109
|
+
async def sed(self, url: str, data: Any = None, options: Optional[dict] = None) -> AsyncResponse:
|
|
110
|
+
return await self._call("sed", url, data, options)
|
|
111
|
+
|
|
112
|
+
async def rm(self, url: str, options: Optional[dict] = None) -> AsyncResponse:
|
|
113
|
+
return await self._call("rm", url, None, options)
|
|
114
|
+
|
|
115
|
+
async def stat(self, url: str, options: Optional[dict] = None) -> AsyncResponse:
|
|
116
|
+
return await self._call("stat", url, None, options)
|
|
117
|
+
|
|
118
|
+
async def man(self, url: str, options: Optional[dict] = None) -> AsyncResponse:
|
|
119
|
+
return await self._call("man", url, None, options)
|
|
120
|
+
|
|
121
|
+
async def _call(self, method: str, url: str, data: Any, options: Optional[dict]) -> AsyncResponse:
|
|
122
|
+
from ._config import merge_config
|
|
123
|
+
|
|
124
|
+
base = merge_config(self.defaults, options or {})
|
|
125
|
+
cfg = dict(base)
|
|
126
|
+
cfg["url"] = url
|
|
127
|
+
cfg["method"] = METHOD_BY_COMMAND[method]
|
|
128
|
+
if data is not None and method not in NO_BODY_METHODS:
|
|
129
|
+
cfg["data"] = data
|
|
130
|
+
cfg.setdefault("validate_status", validate_status_default)
|
|
131
|
+
|
|
132
|
+
def run_request_traps(c):
|
|
133
|
+
for t in self._traps:
|
|
134
|
+
if t["kind"] != "request":
|
|
135
|
+
continue
|
|
136
|
+
if t.get("runWhen") and not t["runWhen"](c):
|
|
137
|
+
continue
|
|
138
|
+
out = t["fn"](c)
|
|
139
|
+
if out is not None:
|
|
140
|
+
c = out
|
|
141
|
+
return c
|
|
142
|
+
|
|
143
|
+
def run_response_traps(r):
|
|
144
|
+
for t in self._traps:
|
|
145
|
+
if t["kind"] != "response":
|
|
146
|
+
continue
|
|
147
|
+
out = t["fn"](r)
|
|
148
|
+
if out is not None:
|
|
149
|
+
r = out
|
|
150
|
+
return r
|
|
151
|
+
|
|
152
|
+
cfg = run_request_traps(cfg)
|
|
153
|
+
raw = await _adispatch(cfg)
|
|
154
|
+
raw = run_response_traps(raw)
|
|
155
|
+
return AsyncResponse(raw)
|
|
156
|
+
|
|
157
|
+
def trap(self, kind: str, fn: Callable, options: Optional[dict] = None) -> dict:
|
|
158
|
+
entry = {"kind": kind, "fn": fn, "runWhen": (options or {}).get("runWhen")}
|
|
159
|
+
self._traps.append(entry)
|
|
160
|
+
return {"eject": lambda: self._remove(entry)}
|
|
161
|
+
|
|
162
|
+
def untrap(self, handle: dict) -> None:
|
|
163
|
+
handle["eject"]()
|
|
164
|
+
|
|
165
|
+
def _remove(self, entry: dict) -> None:
|
|
166
|
+
if entry in self._traps:
|
|
167
|
+
self._traps.remove(entry)
|
|
168
|
+
|
|
169
|
+
def cd(self, base_url: str, options: Optional[dict] = None) -> "AsyncUsrClient":
|
|
170
|
+
from ._config import merge_config
|
|
171
|
+
|
|
172
|
+
merged = merge_config(self.defaults, {**dict(options or {}), "base_url": base_url})
|
|
173
|
+
client = AsyncUsrClient(merged)
|
|
174
|
+
client._traps = list(self._traps)
|
|
175
|
+
return client
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def cd(base_url: str, options: Optional[dict] = None) -> AsyncUsrClient:
|
|
179
|
+
client = AsyncUsrClient({**dict(options or {}), "base_url": base_url})
|
|
180
|
+
return client.cd(base_url, options)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""USR client: commands, scoped clients, defaults, and traps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
6
|
+
|
|
7
|
+
from ._adapter import dispatch
|
|
8
|
+
from ._config import merge_config, validate_status_default
|
|
9
|
+
from ._error import UsrError
|
|
10
|
+
from ._pipe import PipeError, Pipeable, pipeable
|
|
11
|
+
from ._request import NO_BODY_METHODS, METHOD_BY_COMMAND, request as raw_request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Response:
|
|
15
|
+
__slots__ = ("_data", "status", "status_text", "headers", "config", "request")
|
|
16
|
+
|
|
17
|
+
def __init__(self, raw: dict) -> None:
|
|
18
|
+
self._data = raw["data"]
|
|
19
|
+
self.status = raw["status"]
|
|
20
|
+
self.status_text = raw.get("statusText", "")
|
|
21
|
+
self.headers = raw.get("headers", {})
|
|
22
|
+
self.config = raw.get("config", {})
|
|
23
|
+
self.request = raw.get("request")
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def data(self) -> Any:
|
|
27
|
+
return self._data
|
|
28
|
+
|
|
29
|
+
def __or__(self, stage):
|
|
30
|
+
return Pipeable(stage(self._data))
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str: # pragma: no cover - debug aid
|
|
33
|
+
return f"Response(status={self.status}, data={self._data!r})"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UsrClient:
|
|
37
|
+
def __init__(self, base_config: Optional[dict] = None) -> None:
|
|
38
|
+
self.defaults: dict = dict(base_config or {})
|
|
39
|
+
self._traps: list = []
|
|
40
|
+
|
|
41
|
+
def _run_request_traps(self, config: dict) -> dict:
|
|
42
|
+
result = config
|
|
43
|
+
for t in self._traps:
|
|
44
|
+
if t["kind"] != "request":
|
|
45
|
+
continue
|
|
46
|
+
if t.get("runWhen") and not t["runWhen"](result):
|
|
47
|
+
continue
|
|
48
|
+
out = t["fn"](result)
|
|
49
|
+
if out is not None:
|
|
50
|
+
result = out
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def _run_response_traps(self, raw: dict) -> dict:
|
|
54
|
+
result = raw
|
|
55
|
+
for t in self._traps:
|
|
56
|
+
if t["kind"] != "response":
|
|
57
|
+
continue
|
|
58
|
+
out = t["fn"](result)
|
|
59
|
+
if out is not None:
|
|
60
|
+
result = out
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
# -- commands --
|
|
64
|
+
def cat(self, url: str, options: Optional[dict] = None) -> Response:
|
|
65
|
+
return self._call("cat", url, None, options)
|
|
66
|
+
|
|
67
|
+
def touch(self, url: str, data: Any = None, options: Optional[dict] = None) -> Response:
|
|
68
|
+
return self._call("touch", url, data, options)
|
|
69
|
+
|
|
70
|
+
def mv(self, url: str, data: Any = None, options: Optional[dict] = None) -> Response:
|
|
71
|
+
return self._call("mv", url, data, options)
|
|
72
|
+
|
|
73
|
+
def sed(self, url: str, data: Any = None, options: Optional[dict] = None) -> Response:
|
|
74
|
+
return self._call("sed", url, data, options)
|
|
75
|
+
|
|
76
|
+
def rm(self, url: str, options: Optional[dict] = None) -> Response:
|
|
77
|
+
return self._call("rm", url, None, options)
|
|
78
|
+
|
|
79
|
+
def stat(self, url: str, options: Optional[dict] = None) -> Response:
|
|
80
|
+
return self._call("stat", url, None, options)
|
|
81
|
+
|
|
82
|
+
def man(self, url: str, options: Optional[dict] = None) -> Response:
|
|
83
|
+
return self._call("man", url, None, options)
|
|
84
|
+
|
|
85
|
+
def request(self, config: dict) -> Response:
|
|
86
|
+
merged = merge_config(self.defaults, config)
|
|
87
|
+
merged.setdefault("validate_status", validate_status_default)
|
|
88
|
+
merged = self._run_request_traps(merged)
|
|
89
|
+
raw = raw_request(merged)
|
|
90
|
+
raw = self._run_response_traps(raw)
|
|
91
|
+
return Response(raw)
|
|
92
|
+
|
|
93
|
+
def _call(self, method: str, url: str, data: Any, options: Optional[dict]) -> Response:
|
|
94
|
+
base = merge_config(self.defaults, options or {})
|
|
95
|
+
cfg = dict(base)
|
|
96
|
+
cfg["url"] = url
|
|
97
|
+
cfg["method"] = METHOD_BY_COMMAND[method]
|
|
98
|
+
if data is not None and method not in NO_BODY_METHODS:
|
|
99
|
+
cfg["data"] = data
|
|
100
|
+
cfg.setdefault("validate_status", validate_status_default)
|
|
101
|
+
cfg = self._run_request_traps(cfg)
|
|
102
|
+
raw = dispatch(cfg)
|
|
103
|
+
raw = self._run_response_traps(raw)
|
|
104
|
+
return Response(raw)
|
|
105
|
+
|
|
106
|
+
# -- traps --
|
|
107
|
+
def trap(self, kind: str, fn: Callable, options: Optional[dict] = None) -> dict:
|
|
108
|
+
entry = {"kind": kind, "fn": fn, "runWhen": (options or {}).get("runWhen")}
|
|
109
|
+
self._traps.append(entry)
|
|
110
|
+
return {"eject": lambda: self._remove(entry)}
|
|
111
|
+
|
|
112
|
+
def untrap(self, handle: dict) -> None:
|
|
113
|
+
handle["eject"]()
|
|
114
|
+
|
|
115
|
+
def _remove(self, entry: dict) -> None:
|
|
116
|
+
if entry in self._traps:
|
|
117
|
+
self._traps.remove(entry)
|
|
118
|
+
|
|
119
|
+
# -- scoped client --
|
|
120
|
+
def cd(self, base_url: str, options: Optional[dict] = None) -> "UsrClient":
|
|
121
|
+
merged = merge_config(self.defaults, {**dict(options or {}), "base_url": base_url})
|
|
122
|
+
client = UsrClient(merged)
|
|
123
|
+
client._traps = list(self._traps)
|
|
124
|
+
return client
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cd(base_url: str, options: Optional[dict] = None) -> UsrClient:
|
|
128
|
+
client = UsrClient({**dict(options or {}), "base_url": base_url})
|
|
129
|
+
return client.cd(base_url, options)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
defaults: dict = {}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_usr() -> UsrClient:
|
|
136
|
+
return UsrClient()
|