omlish 0.0.0.dev467__py3-none-any.whl → 0.0.0.dev469__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.
@@ -3,13 +3,15 @@
3
3
  import abc
4
4
  import contextlib
5
5
  import dataclasses as dc
6
+ import io
6
7
  import typing as ta
7
8
 
8
9
  from ...lite.abstract import Abstract
9
- from ...lite.dataclasses import dataclass_maybe_post_init
10
10
  from ...lite.dataclasses import dataclass_shallow_asdict
11
+ from .base import BaseHttpClient
11
12
  from .base import BaseHttpResponse
12
13
  from .base import BaseHttpResponseT
14
+ from .base import HttpClientContext
13
15
  from .base import HttpRequest
14
16
  from .base import HttpResponse
15
17
  from .base import HttpStatusError
@@ -26,21 +28,26 @@ HttpClientT = ta.TypeVar('HttpClientT', bound='HttpClient')
26
28
  @dc.dataclass(frozen=True) # kw_only=True
27
29
  class StreamHttpResponse(BaseHttpResponse):
28
30
  class Stream(ta.Protocol):
29
- def read(self, /, n: int = -1) -> bytes: ...
31
+ def read1(self, /, n: int = -1) -> bytes: ...
30
32
 
31
33
  @ta.final
32
34
  class _NullStream:
33
- def read(self, /, n: int = -1) -> bytes:
35
+ def read1(self, /, n: int = -1) -> bytes:
34
36
  raise TypeError
35
37
 
36
38
  stream: Stream = _NullStream()
37
39
 
38
- _closer: ta.Optional[ta.Callable[[], None]] = None
40
+ @property
41
+ def has_data(self) -> bool:
42
+ return not isinstance(self.stream, StreamHttpResponse._NullStream)
43
+
44
+ def read_all(self) -> bytes:
45
+ buf = io.BytesIO()
46
+ while (b := self.stream.read1()):
47
+ buf.write(b)
48
+ return buf.getvalue()
39
49
 
40
- def __post_init__(self) -> None:
41
- dataclass_maybe_post_init(super())
42
- if isinstance(self.stream, StreamHttpResponse._NullStream):
43
- raise TypeError(self.stream)
50
+ _closer: ta.Optional[ta.Callable[[], None]] = None
44
51
 
45
52
  def __enter__(self: StreamHttpResponseT) -> StreamHttpResponseT:
46
53
  return self
@@ -86,10 +93,9 @@ def read_response(resp: BaseHttpResponse) -> HttpResponse:
86
93
  return resp
87
94
 
88
95
  elif isinstance(resp, StreamHttpResponse):
89
- data = resp.stream.read()
90
96
  return HttpResponse(**{
91
97
  **{k: v for k, v in dataclass_shallow_asdict(resp).items() if k not in ('stream', '_closer')},
92
- 'data': data,
98
+ **({'data': resp.read_all()} if resp.has_data else {}),
93
99
  })
94
100
 
95
101
  else:
@@ -99,7 +105,7 @@ def read_response(resp: BaseHttpResponse) -> HttpResponse:
99
105
  ##
100
106
 
101
107
 
102
- class HttpClient(Abstract):
108
+ class HttpClient(BaseHttpClient, Abstract):
103
109
  def __enter__(self: HttpClientT) -> HttpClientT:
104
110
  return self
105
111
 
@@ -110,10 +116,12 @@ class HttpClient(Abstract):
110
116
  self,
111
117
  req: HttpRequest,
112
118
  *,
119
+ context: ta.Optional[HttpClientContext] = None,
113
120
  check: bool = False,
114
121
  ) -> HttpResponse:
115
122
  with closing_response(self.stream_request(
116
123
  req,
124
+ context=context,
117
125
  check=check,
118
126
  )) as resp:
119
127
  return read_response(resp)
@@ -122,9 +130,13 @@ class HttpClient(Abstract):
122
130
  self,
123
131
  req: HttpRequest,
124
132
  *,
133
+ context: ta.Optional[HttpClientContext] = None,
125
134
  check: bool = False,
126
135
  ) -> StreamHttpResponse:
127
- resp = self._stream_request(req)
136
+ if context is None:
137
+ context = HttpClientContext()
138
+
139
+ resp = self._stream_request(context, req)
128
140
 
129
141
  try:
130
142
  if check and not resp.is_success:
@@ -141,5 +153,5 @@ class HttpClient(Abstract):
141
153
  return resp
142
154
 
143
155
  @abc.abstractmethod
144
- def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
156
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
145
157
  raise NotImplementedError
@@ -1,3 +1,5 @@
1
+ # ruff: noqa: UP043 UP045
2
+ # @omlish-lite
1
3
  import http.client
2
4
  import typing as ta
3
5
  import urllib.error
@@ -5,6 +7,7 @@ import urllib.request
5
7
 
6
8
  from ..headers import HttpHeaders
7
9
  from .base import DEFAULT_ENCODING
10
+ from .base import HttpClientContext
8
11
  from .base import HttpClientError
9
12
  from .base import HttpRequest
10
13
  from .sync import HttpClient
@@ -39,7 +42,7 @@ class UrllibHttpClient(HttpClient):
39
42
  data=d,
40
43
  )
41
44
 
42
- def _stream_request(self, req: HttpRequest) -> StreamHttpResponse:
45
+ def _stream_request(self, ctx: HttpClientContext, req: HttpRequest) -> StreamHttpResponse:
43
46
  try:
44
47
  resp = urllib.request.urlopen( # noqa
45
48
  self._build_request(req),
@@ -53,7 +56,6 @@ class UrllibHttpClient(HttpClient):
53
56
  headers=HttpHeaders(e.headers.items()),
54
57
  request=req,
55
58
  underlying=e,
56
- stream=e, # noqa
57
59
  _closer=e.close,
58
60
  )
59
61
 
@@ -116,10 +116,10 @@ class CoroHttpClientConnection:
116
116
  _http_version = 11
117
117
  _http_version_str = 'HTTP/1.1'
118
118
 
119
- http_port: ta.ClassVar[int] = 80
120
- https_port: ta.ClassVar[int] = 443
119
+ HTTP_PORT: ta.ClassVar[int] = 80
120
+ HTTPS_PORT: ta.ClassVar[int] = 443
121
121
 
122
- default_port = http_port
122
+ DEFAULT_PORT: ta.ClassVar[int] = HTTP_PORT
123
123
 
124
124
  class _NOT_SET: # noqa
125
125
  def __new__(cls, *args, **kwargs): # noqa
@@ -139,6 +139,7 @@ class CoroHttpClientConnection:
139
139
  source_address: ta.Optional[str] = None,
140
140
  block_size: int = 8192,
141
141
  auto_open: bool = True,
142
+ default_port: ta.Optional[int] = None,
142
143
  ) -> None:
143
144
  super().__init__()
144
145
 
@@ -146,6 +147,9 @@ class CoroHttpClientConnection:
146
147
  self._source_address = source_address
147
148
  self._block_size = block_size
148
149
  self._auto_open = auto_open
150
+ if default_port is None:
151
+ default_port = self.DEFAULT_PORT
152
+ self._default_port = default_port
149
153
 
150
154
  self._connected = False
151
155
  self._buffer: ta.List[bytes] = []
@@ -162,6 +166,10 @@ class CoroHttpClientConnection:
162
166
 
163
167
  CoroHttpClientValidation.validate_host(self._host)
164
168
 
169
+ @property
170
+ def http_version(self) -> int:
171
+ return self._http_version
172
+
165
173
  #
166
174
 
167
175
  def _get_hostport(self, host: str, port: ta.Optional[int]) -> ta.Tuple[str, int]:
@@ -173,12 +181,12 @@ class CoroHttpClientConnection:
173
181
  port = int(host[i + 1:])
174
182
  except ValueError:
175
183
  if host[i + 1:] == '': # http://foo.com:/ == http://foo.com/
176
- port = self.default_port
184
+ port = self._default_port
177
185
  else:
178
186
  raise CoroHttpClientErrors.InvalidUrlError(f"non-numeric port: '{host[i + 1:]}'") from None
179
187
  host = host[:i]
180
188
  else:
181
- port = self.default_port
189
+ port = self._default_port
182
190
 
183
191
  if host and host[0] == '[' and host[-1] == ']':
184
192
  host = host[1:-1]
@@ -286,6 +294,7 @@ class CoroHttpClientConnection:
286
294
  source_address=self._source_address,
287
295
  **(dict(timeout=self._timeout) if self._timeout is not self._NOT_SET else {}),
288
296
  ),
297
+ server_hostname=self._tunnel_host if self._tunnel_host else self._host,
289
298
  )))
290
299
 
291
300
  self._connected = True
@@ -526,7 +535,7 @@ class CoroHttpClientConnection:
526
535
  if ':' in host:
527
536
  host_enc = self._strip_ipv6_iface(host_enc)
528
537
 
529
- if port == self.default_port:
538
+ if port == self._default_port:
530
539
  self.put_header('Host', host_enc)
531
540
  else:
532
541
  self.put_header('Host', f"{host_enc.decode('ascii')}:{port}")
omlish/http/coro/io.py CHANGED
@@ -37,6 +37,8 @@ class CoroHttpIo:
37
37
  args: ta.Tuple[ta.Any, ...]
38
38
  kwargs: ta.Optional[ta.Dict[str, ta.Any]] = None
39
39
 
40
+ server_hostname: ta.Optional[str] = None
41
+
40
42
  #
41
43
 
42
44
  class CloseIo(Io):
omlish/http/urls.py ADDED
@@ -0,0 +1,67 @@
1
+ # ruff: noqa: UP006 UP007 UP045
2
+ # @omlish-lite
3
+ import re
4
+ import typing as ta
5
+ import urllib.parse
6
+
7
+ from ..lite.cached import cached_nullary
8
+
9
+
10
+ ##
11
+
12
+
13
+ @cached_nullary
14
+ def _url_split_host_pat() -> re.Pattern:
15
+ return re.compile('//([^/#?]*)(.*)', re.DOTALL)
16
+
17
+
18
+ def url_split_host(url: str) -> ta.Tuple[ta.Optional[str], str]:
19
+ """splithost('//host[:port]/path') --> 'host[:port]', '/path'."""
20
+
21
+ # https://github.com/python/cpython/blob/364ae607d8035db8ba92486ebebd8225446c1a90/Lib/urllib/parse.py#L1143
22
+ if not (m := _url_split_host_pat().match(url)):
23
+ return None, url
24
+
25
+ host_port, path = m.groups()
26
+ if path and path[0] != '/':
27
+ path = '/' + path
28
+ return host_port, path
29
+
30
+
31
+ ##
32
+
33
+
34
+ def unparse_url_request_path(url: ta.Union[str, urllib.parse.ParseResult]) -> str:
35
+ if isinstance(url, urllib.parse.ParseResult):
36
+ ups = url
37
+ else:
38
+ ups = urllib.parse.urlparse(url)
39
+
40
+ return urllib.parse.urlunparse((
41
+ '',
42
+ '',
43
+ ups.path,
44
+ ups.params,
45
+ ups.query,
46
+ ups.fragment,
47
+ ))
48
+
49
+
50
+ def parsed_url_replace(
51
+ url: urllib.parse.ParseResult,
52
+ *,
53
+ scheme: ta.Optional[str] = None,
54
+ netloc: ta.Optional[str] = None,
55
+ path: ta.Optional[str] = None,
56
+ params: ta.Optional[str] = None,
57
+ query: ta.Optional[str] = None,
58
+ fragment: ta.Optional[str] = None,
59
+ ) -> urllib.parse.ParseResult:
60
+ return urllib.parse.ParseResult(
61
+ scheme if scheme is not None else url.scheme,
62
+ netloc if netloc is not None else url.netloc,
63
+ path if path is not None else url.path,
64
+ params if params is not None else url.params,
65
+ query if query is not None else url.query,
66
+ fragment if fragment is not None else url.fragment,
67
+ )
omlish/io/buffers.py CHANGED
@@ -208,6 +208,9 @@ class ReadableListBuffer:
208
208
 
209
209
  def read(self, n: ta.Optional[int] = None) -> ta.Optional[bytes]:
210
210
  if n is None:
211
+ if not self._lst:
212
+ return b''
213
+
211
214
  o = b''.join(self._lst)
212
215
  self._lst = []
213
216
  return o
omlish/lang/__init__.py CHANGED
@@ -283,6 +283,7 @@ with _auto_proxy_init(globals(), update_exports=True):
283
283
 
284
284
  new_function,
285
285
  new_function_kwargs,
286
+ copy_function,
286
287
  )
287
288
 
288
289
  from .generators import ( # noqa
@@ -420,6 +421,8 @@ with _auto_proxy_init(globals(), update_exports=True):
420
421
  )
421
422
 
422
423
  from .params import ( # noqa
424
+ CanParamSpec,
425
+
423
426
  Param,
424
427
 
425
428
  VarParam,
omlish/lang/functions.py CHANGED
@@ -250,16 +250,17 @@ def new_function(
250
250
  # a tuple that supplies the bindings for free variables
251
251
  closure: tuple | None = None,
252
252
 
253
- # # a dictionary that specifies the default keyword argument values
254
- # kwdefaults: dict | None = None,
253
+ # a dictionary that specifies the default keyword argument values
254
+ kwdefaults: dict | None = None,
255
255
  ) -> types.FunctionType:
256
+ # https://github.com/python/cpython/blob/9c8eade20c6c6cc6f31dffb5e42472391d63bbf4/Objects/funcobject.c#L909
256
257
  return types.FunctionType(
257
258
  code=code,
258
259
  globals=globals,
259
260
  name=name,
260
261
  argdefs=argdefs,
261
262
  closure=closure,
262
- # kwdefaults=kwdefaults,
263
+ kwdefaults=kwdefaults,
263
264
  )
264
265
 
265
266
 
@@ -270,5 +271,9 @@ def new_function_kwargs(f: types.FunctionType) -> dict[str, ta.Any]:
270
271
  name=f.__name__,
271
272
  argdefs=f.__defaults__,
272
273
  closure=f.__closure__,
273
- # kwdefaults=f.__kwdefaults__,
274
+ kwdefaults=f.__kwdefaults__,
274
275
  )
276
+
277
+
278
+ def copy_function(f: types.FunctionType) -> types.FunctionType:
279
+ return new_function(**new_function_kwargs(f))
omlish/lang/params.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
2
  TODO:
3
3
  - check validity
4
+ - signature vs getfullargspec - diff unwrapping + 'self' handling
4
5
  """
5
6
  import dataclasses as dc
6
7
  import enum
@@ -16,6 +17,13 @@ from .classes.restrict import Sealed
16
17
  T = ta.TypeVar('T')
17
18
 
18
19
 
20
+ CanParamSpec: ta.TypeAlias = ta.Union[
21
+ 'ParamSpec',
22
+ inspect.Signature,
23
+ ta.Callable,
24
+ ]
25
+
26
+
19
27
  ##
20
28
 
21
29
 
@@ -101,6 +109,15 @@ class ParamSpec(ta.Sequence[Param], Final):
101
109
 
102
110
  #
103
111
 
112
+ @classmethod
113
+ def of(cls, obj: CanParamSpec) -> 'ParamSpec':
114
+ if isinstance(obj, ParamSpec):
115
+ return obj
116
+ elif isinstance(obj, inspect.Signature):
117
+ return cls.of_signature(obj)
118
+ else:
119
+ return cls.inspect(obj)
120
+
104
121
  @classmethod
105
122
  def of_signature(
106
123
  cls,
omlish/sync.py CHANGED
@@ -1,3 +1,5 @@
1
+ # ruff: noqa: UP006 UP043 UP045
2
+ # @omlish-lite
1
3
  """
2
4
  TODO:
3
5
  - sync (lol) w/ asyncs.anyio
@@ -8,7 +10,7 @@ import collections
8
10
  import threading
9
11
  import typing as ta
10
12
 
11
- from . import lang
13
+ from .lite.maybes import Maybe
12
14
 
13
15
 
14
16
  T = ta.TypeVar('T')
@@ -17,7 +19,7 @@ T = ta.TypeVar('T')
17
19
  ##
18
20
 
19
21
 
20
- class Once:
22
+ class SyncOnce:
21
23
  def __init__(self) -> None:
22
24
  super().__init__()
23
25
 
@@ -40,43 +42,43 @@ class Once:
40
42
  ##
41
43
 
42
44
 
43
- class Lazy(ta.Generic[T]):
45
+ class SyncLazy(ta.Generic[T]):
44
46
  def __init__(self) -> None:
45
47
  super().__init__()
46
48
 
47
- self._once = Once()
48
- self._v: lang.Maybe[T] = lang.empty()
49
+ self._once = SyncOnce()
50
+ self._v: Maybe[T] = Maybe.empty()
49
51
 
50
- def peek(self) -> lang.Maybe[T]:
52
+ def peek(self) -> Maybe[T]:
51
53
  return self._v
52
54
 
53
55
  def set(self, v: T) -> None:
54
- self._v = lang.just(v)
56
+ self._v = Maybe.just(v)
55
57
 
56
58
  def get(self, fn: ta.Callable[[], T]) -> T:
57
59
  def do():
58
- self._v = lang.just(fn())
60
+ self._v = Maybe.just(fn())
59
61
  self._once.do(do)
60
62
  return self._v.must()
61
63
 
62
64
 
63
- class LazyFn(ta.Generic[T]):
65
+ class SyncLazyFn(ta.Generic[T]):
64
66
  def __init__(self, fn: ta.Callable[[], T]) -> None:
65
67
  super().__init__()
66
68
 
67
69
  self._fn = fn
68
- self._once = Once()
69
- self._v: lang.Maybe[T] = lang.empty()
70
+ self._once = SyncOnce()
71
+ self._v: Maybe[T] = Maybe.empty()
70
72
 
71
- def peek(self) -> lang.Maybe[T]:
73
+ def peek(self) -> Maybe[T]:
72
74
  return self._v
73
75
 
74
76
  def set(self, v: T) -> None:
75
- self._v = lang.just(v)
77
+ self._v = Maybe.just(v)
76
78
 
77
79
  def get(self) -> T:
78
80
  def do():
79
- self._v = lang.just(self._fn())
81
+ self._v = Maybe.just(self._fn())
80
82
  self._once.do(do)
81
83
  return self._v.must()
82
84
 
@@ -89,11 +91,11 @@ class ConditionDeque(ta.Generic[T]):
89
91
  self,
90
92
  *,
91
93
  cond: ta.Optional['threading.Condition'] = None,
92
- deque: collections.deque[T] | None = None,
94
+ deque: ta.Optional[ta.Deque[T]] = None,
93
95
 
94
96
  lock: ta.Optional['threading.RLock'] = None,
95
- maxlen: int | None = None,
96
- init: ta.Iterable[T] | None = None,
97
+ maxlen: ta.Optional[int] = None,
98
+ init: ta.Optional[ta.Iterable[T]] = None,
97
99
  ) -> None:
98
100
  super().__init__()
99
101
 
@@ -112,7 +114,7 @@ class ConditionDeque(ta.Generic[T]):
112
114
  return self._cond
113
115
 
114
116
  @property
115
- def deque(self) -> collections.deque[T]:
117
+ def deque(self) -> ta.Deque[T]:
116
118
  return self._deque
117
119
 
118
120
  def push(
@@ -126,9 +128,9 @@ class ConditionDeque(ta.Generic[T]):
126
128
 
127
129
  def pop(
128
130
  self,
129
- timeout: float | None = None,
131
+ timeout: ta.Optional[float] = None,
130
132
  *,
131
- if_empty: ta.Callable[[], None] | None = None,
133
+ if_empty: ta.Optional[ta.Callable[[], None]] = None,
132
134
  ) -> T:
133
135
  with self.cond:
134
136
  if not self.deque and if_empty is not None:
@@ -141,6 +143,45 @@ class ConditionDeque(ta.Generic[T]):
141
143
  ##
142
144
 
143
145
 
146
+ class SyncBufferRelay(ta.Generic[T]):
147
+ def __init__(
148
+ self,
149
+ *,
150
+ wake_fn: ta.Callable[[], None],
151
+ ) -> None:
152
+ super().__init__()
153
+
154
+ self._wake_fn = wake_fn
155
+
156
+ self._lock = threading.Lock()
157
+ self._buffer: SyncBufferRelay._Buffer = SyncBufferRelay._Buffer()
158
+
159
+ class _Buffer:
160
+ def __init__(self) -> None:
161
+ self.lst: list = []
162
+ self.age = 0
163
+
164
+ def __repr__(self) -> str:
165
+ return f'{self.__class__.__qualname__}({self.lst!r}, age={self.age!r})'
166
+
167
+ def push(self, *vs: T) -> None:
168
+ with self._lock:
169
+ buf = self._buffer
170
+ needs_wake = not buf.age
171
+ buf.lst.extend(vs)
172
+ buf.age += 1
173
+ if needs_wake:
174
+ self._wake_fn()
175
+
176
+ def swap(self) -> ta.Sequence[T]:
177
+ with self._lock:
178
+ buf, self._buffer = self._buffer, SyncBufferRelay._Buffer()
179
+ return buf.lst
180
+
181
+
182
+ ##
183
+
184
+
144
185
  class CountDownLatch:
145
186
  def __init__(self, count: int) -> None:
146
187
  super().__init__()
@@ -156,7 +197,7 @@ class CountDownLatch:
156
197
 
157
198
  def wait(
158
199
  self,
159
- timeout: float | None = None,
200
+ timeout: ta.Optional[float] = None,
160
201
  ) -> bool:
161
202
  with self._cond:
162
203
  return self._cond.wait_for(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omlish
3
- Version: 0.0.0.dev467
3
+ Version: 0.0.0.dev469
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License-Expression: BSD-3-Clause