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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()