httpee 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.
- httpee/__init__.py +204 -0
- httpee-0.1.0.dist-info/METADATA +175 -0
- httpee-0.1.0.dist-info/RECORD +6 -0
- httpee-0.1.0.dist-info/WHEEL +5 -0
- httpee-0.1.0.dist-info/licenses/LICENSE +21 -0
- httpee-0.1.0.dist-info/top_level.txt +1 -0
httpee/__init__.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""httpee — HTTP для людей. Теперь пайплайнами.
|
|
2
|
+
|
|
3
|
+
Тонкая обёртка над httpx для eelang: запрос собирается пайплайном, результат
|
|
4
|
+
проверяется паттернами::
|
|
5
|
+
|
|
6
|
+
from httpee import POST, headers, Response
|
|
7
|
+
|
|
8
|
+
user =
|
|
9
|
+
{'name': 'kenneth', 'role': 'admin'}
|
|
10
|
+
..headers: {'Authorization': 'Bearer ...'}
|
|
11
|
+
..POST: 'https://api.example.com/users'
|
|
12
|
+
..assert:
|
|
13
|
+
Response(status=201)
|
|
14
|
+
|
|
15
|
+
page =
|
|
16
|
+
httpee()
|
|
17
|
+
..GET: 'https://api.example.com/users'
|
|
18
|
+
..assert:
|
|
19
|
+
Response(status=200)
|
|
20
|
+
|
|
21
|
+
4xx/5xx исключений не вызывают — статус проверяется ``..assert:``/``..match:``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import httpx
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Response(httpx.Response):
|
|
30
|
+
"""httpx.Response + свойство ``status`` для паттерна ``Response(status=200)``.
|
|
31
|
+
|
|
32
|
+
Остальное — родное: ``.json()``, ``.text``, ``.content``, ``.headers``,
|
|
33
|
+
``.raise_for_status()``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def status(self):
|
|
38
|
+
return self.status_code
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Request:
|
|
42
|
+
"""Накопленное состояние запроса. Пользователю не виден."""
|
|
43
|
+
|
|
44
|
+
__slots__ = ('method', 'url', 'body', 'json_body', 'headers', 'params',
|
|
45
|
+
'timeout', 'cookies', 'auth')
|
|
46
|
+
|
|
47
|
+
def __init__(self, body=None):
|
|
48
|
+
self.method = None
|
|
49
|
+
self.url = None
|
|
50
|
+
self.body = body
|
|
51
|
+
self.json_body = None
|
|
52
|
+
self.headers = {}
|
|
53
|
+
self.params = {}
|
|
54
|
+
self.timeout = None
|
|
55
|
+
self.cookies = None
|
|
56
|
+
self.auth = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def httpee():
|
|
60
|
+
"""Пустой запрос: httpee() ..GET: url — когда тела нет."""
|
|
61
|
+
return _Request()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _ensure(value):
|
|
65
|
+
"""Тело или _Request → _Request; на Response — внятный TypeError."""
|
|
66
|
+
if isinstance(value, Response):
|
|
67
|
+
raise TypeError(
|
|
68
|
+
'это уже Response — модификаторы и глаголы идут до выполнения запроса')
|
|
69
|
+
return value if isinstance(value, _Request) else _Request(body=value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _wrap(resp):
|
|
73
|
+
"""httpx.Response → httpee.Response (для class-паттернов)."""
|
|
74
|
+
return Response(
|
|
75
|
+
resp.status_code,
|
|
76
|
+
headers=resp.headers,
|
|
77
|
+
content=resp.content,
|
|
78
|
+
request=resp.request,
|
|
79
|
+
extensions=resp.extensions,
|
|
80
|
+
history=resp.history,
|
|
81
|
+
default_encoding=resp.default_encoding,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _execute(req):
|
|
86
|
+
"""Отправить _Request новым httpx.Client и вернуть Response."""
|
|
87
|
+
if req.url is None:
|
|
88
|
+
raise TypeError(
|
|
89
|
+
'запрос без адреса: задайте ..url: или вызовите ..GET:/..POST: с адресом')
|
|
90
|
+
method = (req.method or 'GET').upper()
|
|
91
|
+
kwargs = {}
|
|
92
|
+
if req.json_body is not None:
|
|
93
|
+
kwargs['json'] = req.json_body
|
|
94
|
+
else:
|
|
95
|
+
body = req.body
|
|
96
|
+
if body is None:
|
|
97
|
+
pass
|
|
98
|
+
elif isinstance(body, dict):
|
|
99
|
+
kwargs['data'] = body
|
|
100
|
+
elif isinstance(body, str):
|
|
101
|
+
kwargs['content'] = body.encode()
|
|
102
|
+
elif isinstance(body, (bytes, bytearray, memoryview)):
|
|
103
|
+
kwargs['content'] = bytes(body)
|
|
104
|
+
else:
|
|
105
|
+
raise TypeError(
|
|
106
|
+
f'неподдерживаемое тело запроса: {type(body).__name__} '
|
|
107
|
+
'(ожидаются dict/str/bytes)')
|
|
108
|
+
with httpx.Client(timeout=req.timeout) as client:
|
|
109
|
+
resp = client.request(method, req.url, headers=req.headers or None,
|
|
110
|
+
params=req.params or None, cookies=req.cookies,
|
|
111
|
+
auth=req.auth, **kwargs)
|
|
112
|
+
return _wrap(resp)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _verb(method):
|
|
116
|
+
"""Глагол: ..GET: url → GET(url, значение) — выполняет запрос сразу."""
|
|
117
|
+
|
|
118
|
+
def run(url, value):
|
|
119
|
+
req = _ensure(value)
|
|
120
|
+
req.method = method
|
|
121
|
+
req.url = url
|
|
122
|
+
return _execute(req)
|
|
123
|
+
|
|
124
|
+
run.__name__ = method
|
|
125
|
+
run.__qualname__ = method
|
|
126
|
+
run.__doc__ = (
|
|
127
|
+
f'Выполнить {method}-запрос: ..{method}: url. '
|
|
128
|
+
'Значение пайплайна — тело или накопленный запрос.')
|
|
129
|
+
return run
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
GET = _verb('GET')
|
|
133
|
+
POST = _verb('POST')
|
|
134
|
+
PUT = _verb('PUT')
|
|
135
|
+
PATCH = _verb('PATCH')
|
|
136
|
+
DELETE = _verb('DELETE')
|
|
137
|
+
HEAD = _verb('HEAD')
|
|
138
|
+
OPTIONS = _verb('OPTIONS')
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def send(value):
|
|
142
|
+
"""Выполнить запрос с заранее заданными ..method:/..url: (..send())."""
|
|
143
|
+
return _execute(_ensure(value))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def headers(headers_value, value):
|
|
147
|
+
"""..headers: {...} — заголовки запроса (повторные вызовы дополняют)."""
|
|
148
|
+
req = _ensure(value)
|
|
149
|
+
req.headers.update(dict(headers_value))
|
|
150
|
+
return req
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def query(params_value, value):
|
|
154
|
+
"""..query: {...} — query-параметры URL (повторные вызовы дополняют)."""
|
|
155
|
+
req = _ensure(value)
|
|
156
|
+
req.params.update(dict(params_value))
|
|
157
|
+
return req
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def timeout(timeout_value, value):
|
|
161
|
+
"""..timeout: 5.0 — таймаут запроса в секундах."""
|
|
162
|
+
req = _ensure(value)
|
|
163
|
+
req.timeout = timeout_value
|
|
164
|
+
return req
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def cookies(cookies_value, value):
|
|
168
|
+
"""..cookies: {...} — куки запроса."""
|
|
169
|
+
req = _ensure(value)
|
|
170
|
+
req.cookies = cookies_value
|
|
171
|
+
return req
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def auth(auth_value, value):
|
|
175
|
+
"""..auth: ('user', 'pass') — basic-auth; можно httpx.Auth."""
|
|
176
|
+
req = _ensure(value)
|
|
177
|
+
req.auth = auth_value
|
|
178
|
+
return req
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def json(json_value, value):
|
|
182
|
+
"""..json: {...} — тело JSON-ом (побеждает над телом пайплайна)."""
|
|
183
|
+
req = _ensure(value)
|
|
184
|
+
req.json_body = json_value
|
|
185
|
+
return req
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def method(method_value, value):
|
|
189
|
+
"""..method: 'PATCH' — метод заранее; выполняется ..send()."""
|
|
190
|
+
req = _ensure(value)
|
|
191
|
+
req.method = str(method_value).upper()
|
|
192
|
+
return req
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def url(url_value, value):
|
|
196
|
+
"""..url: 'https://...' — адрес заранее; выполняется ..send()."""
|
|
197
|
+
req = _ensure(value)
|
|
198
|
+
req.url = url_value
|
|
199
|
+
return req
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS',
|
|
203
|
+
'send', 'headers', 'query', 'timeout', 'cookies', 'auth', 'json',
|
|
204
|
+
'method', 'url', 'Response', 'httpee']
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: httpee
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: httpee — thin httpx wrapper for eelang pipelines
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# httpee
|
|
13
|
+
|
|
14
|
+
`httpee` is a thin wrapper around [httpx](https://www.python-httpx.org/) for
|
|
15
|
+
[eelang](https://github.com/eel-lang/eelang-py): requests are written as pipelines,
|
|
16
|
+
responses are checked with patterns.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from httpee import POST, headers, httpee, Response
|
|
20
|
+
|
|
21
|
+
user =
|
|
22
|
+
{'name': 'kenneth', 'role': 'admin'} # dict → form-encoded, like requests data=
|
|
23
|
+
..headers: {'Authorization': f'Bearer {token}'}
|
|
24
|
+
..POST: 'https://api.example.com/users'
|
|
25
|
+
..assert:
|
|
26
|
+
Response(status=201)
|
|
27
|
+
|
|
28
|
+
page =
|
|
29
|
+
httpee() # empty request — no body
|
|
30
|
+
..query: {'page': 2}
|
|
31
|
+
..GET: 'https://api.example.com/users'
|
|
32
|
+
..assert:
|
|
33
|
+
Response(status=200)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Cheatsheet
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from httpee import (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, send,
|
|
40
|
+
httpee, headers, query, timeout, cookies, auth, json,
|
|
41
|
+
method, url, Response)
|
|
42
|
+
from eelang import MatchError
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### A request is a pipeline
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
r =
|
|
49
|
+
{'name': 'kenneth', 'role': 'admin'} # dict → form-encoded
|
|
50
|
+
..headers: {'X-Key': v} # headers (repeats add keys)
|
|
51
|
+
..query: {'page': 2} # query parameters (repeats add keys)
|
|
52
|
+
..POST: 'https://api.example.com/users' # verb performs the request
|
|
53
|
+
..assert:
|
|
54
|
+
Response(status=201) # failure → MatchError
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Bodies
|
|
58
|
+
|
|
59
|
+
| Value | Sent as |
|
|
60
|
+
|---|---|
|
|
61
|
+
| dict | form-encoded (`data=`, like requests) |
|
|
62
|
+
| str | text |
|
|
63
|
+
| bytes | raw bytes |
|
|
64
|
+
| `httpee()` | empty request — no body |
|
|
65
|
+
| `..json: {…}` | JSON — takes precedence over the pipeline body |
|
|
66
|
+
|
|
67
|
+
### Dynamic method — `..send()`
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
r =
|
|
71
|
+
{'role': 'moderator'}
|
|
72
|
+
..method: m # any method, even from a variable
|
|
73
|
+
..url: 'https://…'
|
|
74
|
+
..send() # without ..method: defaults to GET
|
|
75
|
+
..assert:
|
|
76
|
+
Response(status=200)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Pattern checks
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# ..assert: — shape check, binds variables, failure → MatchError
|
|
83
|
+
p =
|
|
84
|
+
httpee()
|
|
85
|
+
..GET: url
|
|
86
|
+
..assert:
|
|
87
|
+
Response(status=s) # s is available afterwards
|
|
88
|
+
|
|
89
|
+
# ..match: — branch on status; no match → the value is unchanged
|
|
90
|
+
verdict =
|
|
91
|
+
httpee()
|
|
92
|
+
..GET: url
|
|
93
|
+
..match:
|
|
94
|
+
case Response(status=200): 'ok'
|
|
95
|
+
case Response(status=n): f'HTTP {n}'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Errors
|
|
99
|
+
|
|
100
|
+
- 4xx/5xx do **not** raise: check the status with a pattern.
|
|
101
|
+
- Network errors and timeouts are httpx exceptions (`ConnectError`, `TimeoutException`).
|
|
102
|
+
- `Response` is `httpx.Response` plus `.status`: `.json()`, `.text`, `.headers` work as usual.
|
|
103
|
+
|
|
104
|
+
### Modifiers at a glance
|
|
105
|
+
|
|
106
|
+
`..headers:` · `..query:` · `..timeout: 5.0` · `..cookies:` · `..auth: (u, p)` · `..json:` · `..method:` · `..url:`
|
|
107
|
+
|
|
108
|
+
## How it works
|
|
109
|
+
|
|
110
|
+
- The pipeline's initial value is the request body: dict → form-encoded, str → text,
|
|
111
|
+
bytes → raw bytes. JSON goes through an explicit `..json: {…}`. A request without
|
|
112
|
+
a body starts with `httpee()`.
|
|
113
|
+
- Modifiers accumulate parameters (repeatable, in any order); the verb at the end
|
|
114
|
+
performs the request and returns a `Response`.
|
|
115
|
+
- 4xx/5xx do not raise — the status is checked with a pattern; a failed `..assert:`
|
|
116
|
+
raises `MatchError`. Network errors and timeouts are httpx exceptions.
|
|
117
|
+
|
|
118
|
+
## Verbs
|
|
119
|
+
|
|
120
|
+
| Verb | Example | What it does |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `GET` … `OPTIONS` | `..GET: url` | performs the request with this method |
|
|
123
|
+
| `send` | `..send()` | performs the request with a previously set `..method:`/`..url:` |
|
|
124
|
+
|
|
125
|
+
The method can be set up front — and can be anything:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
patch =
|
|
129
|
+
{'role': 'moderator'}
|
|
130
|
+
..method: 'PATCH' # or from a variable
|
|
131
|
+
..url: 'https://api.example.com/users/1'
|
|
132
|
+
..send()
|
|
133
|
+
..assert:
|
|
134
|
+
Response(status=200)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Modifiers
|
|
138
|
+
|
|
139
|
+
| Modifier | Example | What it does |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `headers` | `..headers: {'X-Key': v}` | headers (repeats add keys) |
|
|
142
|
+
| `query` | `..query: {'page': 2}` | URL query parameters (repeats add keys) |
|
|
143
|
+
| `timeout` | `..timeout: 5.0` | request timeout, seconds |
|
|
144
|
+
| `cookies` | `..cookies: {'sid': v}` | cookies |
|
|
145
|
+
| `auth` | `..auth: ('user', 'pass')` | basic auth (any `httpx.Auth` works) |
|
|
146
|
+
| `json` | `..json: {…}` | JSON body (takes precedence over the pipeline body) |
|
|
147
|
+
| `method` | `..method: 'PATCH'` | method up front — for `..send()` |
|
|
148
|
+
| `url` | `..url: 'https://…'` | URL up front — for `..send()` |
|
|
149
|
+
|
|
150
|
+
## Installation
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
pip install httpee
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
From source:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
git clone https://github.com/eel-lang/httpee
|
|
160
|
+
pip install ./httpee
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Requires CPython 3.15 + [eelang](https://github.com/eel-lang/eelang-py) to run `.eel`
|
|
164
|
+
files — the package itself is plain Python, and httpx is its only dependency.
|
|
165
|
+
Works under gevent (`gevent.monkey.patch_all()`).
|
|
166
|
+
|
|
167
|
+
## Tests
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
python -m unittest discover -s tests
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT (`LICENSE`).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
httpee/__init__.py,sha256=7J7iS6VTl8dG970V0ShkU5JneNXWkfOgozDqxGowIe4,6421
|
|
2
|
+
httpee-0.1.0.dist-info/licenses/LICENSE,sha256=e5O-ASdm-ay53Ombr3FGTuTCHitjde-7X9cOqWEA5nw,1063
|
|
3
|
+
httpee-0.1.0.dist-info/METADATA,sha256=LfuLdXYEz79ScK68GQLAPnNcdUuwJ7s7NjPAka2jKig,5125
|
|
4
|
+
httpee-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
httpee-0.1.0.dist-info/top_level.txt,sha256=4CRnPD5HgDwCZ2xgs5z306QWHMgAGNGzZsk7-5VT4V4,7
|
|
6
|
+
httpee-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pwtail
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
httpee
|