reclie 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.
reclie-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EmekadeFirst
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,7 @@
1
+ # Ship the Zig sources + build script in the sdist so a source build can run
2
+ # `zig build` (requires Zig on the build machine).
3
+ include build.zig
4
+ recursive-include src *.zig *.c
5
+ include LICENSE README.md
6
+ # Do not ship prebuilt binaries in the sdist.
7
+ recursive-exclude reclie *.pyd *.so
reclie-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: reclie
3
+ Version: 0.1.0
4
+ Summary: A high-performance async HTTP client with a native Zig core.
5
+ Author: Victor Chibuogwu Chukwuemeka
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/emekadefirst/reclie
8
+ Keywords: http,https,async,client,zig,tls
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: certifi
21
+ Dynamic: license-file
22
+
23
+ Reclie - pronouced re -cly
24
+
25
+ A High peformance python request client that allows developer to make request in python, utilizing the python simplicity and leveraging on zig to do the heavy lifting, and also bypassing python runtime gil, buffer and bytecode delay
26
+
27
+
28
+ with the aim of outperforming all existing client in python. makking super fast light and clean
29
+
30
+ First is to start with HTTP, HTTPS, and support HTTP2
31
+
32
+ Next SSE
33
+
34
+ Lastly WebSocker WS WSS
reclie-0.1.0/README.md ADDED
@@ -0,0 +1,12 @@
1
+ Reclie - pronouced re -cly
2
+
3
+ A High peformance python request client that allows developer to make request in python, utilizing the python simplicity and leveraging on zig to do the heavy lifting, and also bypassing python runtime gil, buffer and bytecode delay
4
+
5
+
6
+ with the aim of outperforming all existing client in python. makking super fast light and clean
7
+
8
+ First is to start with HTTP, HTTPS, and support HTTP2
9
+
10
+ Next SSE
11
+
12
+ Lastly WebSocker WS WSS
reclie-0.1.0/build.zig ADDED
@@ -0,0 +1,101 @@
1
+ //! Build the reclie native engine (`_reclie`) as a CPython extension.
2
+ //!
3
+ //! Strategy (see src/root.zig): hand-declared Zig externs for the ABI-stable
4
+ //! CPython functions, plus one tiny C shim (`src/py_module.c`) the C compiler
5
+ //! expands. Targets Py_LIMITED_API 3.11, so a single abi3 wheel serves 3.11+.
6
+ //!
7
+ //! Linking libpython differs per OS:
8
+ //! * Windows - link `python3.lib` (the limited-API import lib).
9
+ //! * else - do NOT link libpython; the interpreter resolves those
10
+ //! symbols at import time (`linker_allow_shlib_undefined`).
11
+ //!
12
+ //! Options:
13
+ //! -Dpython="C:/path/PythonXY" convenience: derives Include/ and libs/
14
+ //! -Dpython-include="/path" explicit dir containing Python.h
15
+ //! -Dpython-libs="/path" explicit dir with python3.lib (Windows)
16
+ //!
17
+ //! Output: the built shared library is copied into the package as
18
+ //! `reclie/_reclie.pyd` (Windows) or `reclie/_reclie.abi3.so` (else).
19
+
20
+ const std = @import("std");
21
+
22
+ pub fn build(b: *std.Build) void {
23
+ const target = b.standardTargetOptions(.{});
24
+ const optimize = b.standardOptimizeOption(.{});
25
+ const os_tag = target.result.os.tag;
26
+
27
+ const py_prefix = b.option(
28
+ []const u8,
29
+ "python",
30
+ "Python install prefix (Windows layout: contains Include/ and libs/)",
31
+ ) orelse "C:/Users/PC/AppData/Local/Programs/Python/Python311";
32
+
33
+ const py_include = b.option(
34
+ []const u8,
35
+ "python-include",
36
+ "Directory containing Python.h",
37
+ ) orelse b.pathJoin(&.{ py_prefix, "Include" });
38
+
39
+ const py_libs = b.option(
40
+ []const u8,
41
+ "python-libs",
42
+ "Directory containing python3.lib (Windows only)",
43
+ ) orelse b.pathJoin(&.{ py_prefix, "libs" });
44
+
45
+ const mod = b.createModule(.{
46
+ .root_source_file = b.path("src/root.zig"),
47
+ .target = target,
48
+ .optimize = optimize,
49
+ .link_libc = true,
50
+ });
51
+
52
+ // CPython headers for the C shim.
53
+ mod.addIncludePath(.{ .cwd_relative = py_include });
54
+ mod.addCSourceFile(.{ .file = b.path("src/py_module.c") });
55
+
56
+ if (os_tag == .windows) {
57
+ // Windows requires linking the limited-API import library.
58
+ mod.addLibraryPath(.{ .cwd_relative = py_libs });
59
+ // `.use_pkg_config = .no`: without this, Zig may consult a stray
60
+ // pkg-config on the machine (e.g. Strawberry Perl's on GitHub's Windows
61
+ // runners) and wrongly link `python3.9`. We just want `python3.lib`
62
+ // from the path above.
63
+ mod.linkSystemLibrary("python3", .{ .use_pkg_config = .no }); // -> python3.lib -> python3.dll
64
+ }
65
+
66
+ const lib = b.addLibrary(.{
67
+ .name = "_reclie",
68
+ .linkage = .dynamic,
69
+ .root_module = mod,
70
+ });
71
+
72
+ if (os_tag != .windows) {
73
+ // libpython symbols are undefined until the interpreter loads us. This
74
+ // emits `-fallow-shlib-undefined`, which on ELF permits the undefined
75
+ // symbols and on Mach-O maps to `-undefined dynamic_lookup` (verified:
76
+ // a macOS-targeted dynamic lib with an undefined extern links only with
77
+ // this set). So both Linux and macOS are covered.
78
+ lib.linker_allow_shlib_undefined = true;
79
+ }
80
+
81
+ // Copy the built shared library into the Python package under its abi3
82
+ // extension name so `from reclie import _reclie` resolves and packaging
83
+ // picks it up.
84
+ const ext_name = if (os_tag == .windows)
85
+ "reclie/_reclie.pyd"
86
+ else
87
+ "reclie/_reclie.abi3.so";
88
+
89
+ const install_ext = b.addUpdateSourceFiles();
90
+ install_ext.addCopyFileToSource(lib.getEmittedBin(), ext_name);
91
+ install_ext.step.dependOn(&lib.step);
92
+ b.getInstallStep().dependOn(&install_ext.step);
93
+
94
+ b.installArtifact(lib);
95
+
96
+ // `zig build test` — run the bridge + core unit tests.
97
+ const tests = b.addTest(.{ .root_module = mod });
98
+ const run_tests = b.addRunArtifact(tests);
99
+ const test_step = b.step("test", "Run unit tests");
100
+ test_step.dependOn(&run_tests.step);
101
+ }
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "reclie"
7
+ version = "0.1.0"
8
+ description = "A high-performance async HTTP client with a native Zig core."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Victor Chibuogwu Chukwuemeka" }]
13
+ keywords = ["http", "https", "async", "client", "zig", "tls"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Operating System :: MacOS",
22
+ "Operating System :: Microsoft :: Windows",
23
+ ]
24
+ dependencies = ["certifi"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/emekadefirst/reclie"
28
+
29
+ [tool.setuptools]
30
+ # The compiled extension is produced by `zig build` and placed into the wheel
31
+ # by setup.py's build_ext (into build_lib), so it does NOT need to be listed as
32
+ # package-data — and listing it would wrongly ship a stale platform binary
33
+ # (e.g. a Windows _reclie.pyd) that happens to sit in the source tree.
34
+ packages = ["reclie", "reclie.core", "reclie.utils"]
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # cibuildwheel: build one abi3 wheel per platform (cp311 covers 3.11+).
38
+ # ---------------------------------------------------------------------------
39
+ [tool.cibuildwheel]
40
+ build = "cp311-*"
41
+ skip = ["*-musllinux*", "*_i686", "*-win32"]
42
+ build-frontend = "build"
43
+ # Zig is installed per-OS by the workflow / before-all; confirm it's visible.
44
+ before-build = "zig version"
45
+ test-command = "python -c \"import reclie; print('reclie', reclie.__all__)\""
46
+
47
+ [tool.cibuildwheel.linux]
48
+ # manylinux containers don't have Zig; fetch a pinned build into the container.
49
+ before-all = "bash {project}/.github/install-zig.sh"
50
+ # RECLIE_ZIG_TARGET pins glibc to the manylinux policy so auditwheel accepts the
51
+ # wheel (Zig otherwise emits glibc-2.36 relocations even in the 2.28 container).
52
+ environment = { PATH = "/opt/zig:$PATH", RECLIE_ZIG_TARGET = "x86_64-linux-gnu.2.28" }
53
+ # Alias (cibuildwheel appends the arch -> quay.io/pypa/manylinux_2_28_x86_64).
54
+ # auditwheel still runs to retag the wheel manylinux; our .so has no third-party
55
+ # shared libs to bundle (only libpython, resolved by the interpreter).
56
+ manylinux-x86_64-image = "manylinux_2_28"
57
+
58
+ [tool.cibuildwheel.macos]
59
+ # The extension has no third-party dylibs to bundle and the build already tags
60
+ # the wheel macosx_*; delocate has nothing to do, so skip it.
61
+ repair-wheel-command = ""
62
+
63
+ [tool.uv.workspace]
64
+ members = [
65
+ "example",
66
+ ]
@@ -0,0 +1,40 @@
1
+ """reclie — a high-performance async HTTP client with a native Zig core.
2
+
3
+ v0.1 scope: HTTP/1.1 and HTTP/2 over ``http`` and ``https``
4
+
5
+ Public surface::
6
+
7
+ import reclie
8
+
9
+ client = reclie.Client("api.example.com")
10
+ resp = await client.get("/products")
11
+ data = await resp.json()
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from ._client import Client
17
+ from .core import Method
18
+ from .utils import (
19
+ ConnectionError,
20
+ PoolExhaustedError,
21
+ ProtocolError,
22
+ RecliError,
23
+ RecliHeaders,
24
+ RecliResponse,
25
+ TimeoutError,
26
+ TlsError,
27
+ )
28
+
29
+ __all__ = [
30
+ "Client",
31
+ "Method",
32
+ "RecliResponse",
33
+ "RecliHeaders",
34
+ "RecliError",
35
+ "ConnectionError",
36
+ "TimeoutError",
37
+ "TlsError",
38
+ "ProtocolError",
39
+ "PoolExhaustedError",
40
+ ]
@@ -0,0 +1,183 @@
1
+ """The reclie Client facade.
2
+
3
+ A single, long-lived ``Client`` exposes HTTP/1.1 and HTTP/2 over one
4
+ connection pool. The Python layer is intentionally thin: request
5
+ orchestration lives in :mod:`reclie.core`; this class just owns the pool and
6
+ dispatches to it. (SSE and WebSocket are deferred to a later phase.)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Optional
12
+
13
+ from .core import Headers, Method, Params, request
14
+ from .utils import RecliResponse, TlsError, extension
15
+
16
+ __all__ = ["Client", "Method"]
17
+
18
+
19
+ def _default_ca_bundle_path() -> Optional[str]:
20
+ """Return a path to a CA bundle file. Tries ``certifi`` first, then
21
+ ``ssl.get_default_verify_paths().cafile``. Returns ``None`` if neither is
22
+ available — Client then refuses to construct with ``tls=True``."""
23
+ try:
24
+ import certifi
25
+
26
+ return certifi.where()
27
+ except ImportError:
28
+ try:
29
+ import ssl
30
+
31
+ return ssl.get_default_verify_paths().cafile
32
+ except Exception:
33
+ return None
34
+
35
+
36
+ class Client:
37
+ """Origin-bound, reusable async client for HTTP/1.1 and HTTP/2.
38
+
39
+ Instantiating a ``Client`` allocates the native connection pool. Keep it
40
+ alive for the lifetime of your application; do not create one per request.
41
+ """
42
+
43
+ __slots__ = ("host", "port", "tls", "version", "pool_size", "ttl", "timeout", "_ca_path", "_pool")
44
+
45
+ def __init__(
46
+ self,
47
+ host: str,
48
+ *,
49
+ port: Optional[int] = None,
50
+ tls: Optional[bool] = None,
51
+ version: int = 2,
52
+ pool_size: int = 64,
53
+ ttl: int = 60,
54
+ timeout: float = 30.0,
55
+ ca_bundle: Optional[bytes] = None,
56
+ ca_bundle_path: Optional[str] = None,
57
+ ) -> None:
58
+ if version not in (1, 2):
59
+ raise ValueError("version must be 1 or 2")
60
+
61
+ # Scheme/port inference: https on 443, http otherwise.
62
+ if tls is None:
63
+ tls = port is None or port == 443
64
+ if port is None:
65
+ port = 443 if tls else 80
66
+
67
+ self.host = host
68
+ self.port = port
69
+ self.tls = tls
70
+ self.version = version
71
+ self.pool_size = pool_size
72
+ self.ttl = ttl
73
+ self.timeout = timeout
74
+
75
+ # Resolve a CA bundle *file path* for TLS (the native engine loads it).
76
+ # Precedence:
77
+ # 1. Caller-supplied ``ca_bundle_path``
78
+ # 2. Caller-supplied ``ca_bundle`` (raw PEM bytes -> temp file)
79
+ # 3. ``certifi.where()`` if installed
80
+ # 4. ``ssl.get_default_verify_paths().cafile``
81
+ if not tls:
82
+ self._ca_path = ""
83
+ elif ca_bundle_path is not None:
84
+ self._ca_path = ca_bundle_path
85
+ elif ca_bundle is not None:
86
+ import tempfile
87
+
88
+ tmp = tempfile.NamedTemporaryFile(
89
+ prefix="reclie-ca-", suffix=".pem", delete=False
90
+ )
91
+ tmp.write(ca_bundle)
92
+ tmp.close()
93
+ self._ca_path = tmp.name
94
+ else:
95
+ self._ca_path = _default_ca_bundle_path() or ""
96
+
97
+ if tls and not self._ca_path:
98
+ raise TlsError(
99
+ "No CA bundle available for TLS. Install `certifi` or pass "
100
+ "`ca_bundle`/`ca_bundle_path` to Client(...)."
101
+ )
102
+
103
+ self._pool = extension().pool_create(
104
+ host, port, tls, version, pool_size, ttl, self._timeout_ms,
105
+ self._ca_path,
106
+ )
107
+
108
+ @property
109
+ def _timeout_ms(self) -> int:
110
+ return int(self.timeout * 1000)
111
+
112
+ # ---- HTTP -----------------------------------------------------------
113
+
114
+ async def get(
115
+ self,
116
+ path: str,
117
+ *,
118
+ headers: Optional[Headers] = None,
119
+ params: Optional[Params] = None,
120
+ ) -> RecliResponse:
121
+ return await request(
122
+ self._pool, Method.GET, path,
123
+ version=self.version, timeout_ms=self._timeout_ms,
124
+ headers=headers, params=params,
125
+ )
126
+
127
+ async def post(
128
+ self,
129
+ path: str,
130
+ *,
131
+ json: Optional[Any] = None,
132
+ data: Optional[bytes] = None,
133
+ headers: Optional[Headers] = None,
134
+ params: Optional[Params] = None,
135
+ ) -> RecliResponse:
136
+ return await request(
137
+ self._pool, Method.POST, path,
138
+ version=self.version, timeout_ms=self._timeout_ms,
139
+ headers=headers, params=params, json=json, data=data,
140
+ )
141
+
142
+ async def put(
143
+ self,
144
+ path: str,
145
+ *,
146
+ json: Optional[Any] = None,
147
+ data: Optional[bytes] = None,
148
+ headers: Optional[Headers] = None,
149
+ params: Optional[Params] = None,
150
+ ) -> RecliResponse:
151
+ return await request(
152
+ self._pool, Method.PUT, path,
153
+ version=self.version, timeout_ms=self._timeout_ms,
154
+ headers=headers, params=params, json=json, data=data,
155
+ )
156
+
157
+ async def patch(
158
+ self,
159
+ path: str,
160
+ *,
161
+ json: Optional[Any] = None,
162
+ data: Optional[bytes] = None,
163
+ headers: Optional[Headers] = None,
164
+ params: Optional[Params] = None,
165
+ ) -> RecliResponse:
166
+ return await request(
167
+ self._pool, Method.PATCH, path,
168
+ version=self.version, timeout_ms=self._timeout_ms,
169
+ headers=headers, params=params, json=json, data=data,
170
+ )
171
+
172
+ async def delete(
173
+ self,
174
+ path: str,
175
+ *,
176
+ headers: Optional[Headers] = None,
177
+ params: Optional[Params] = None,
178
+ ) -> RecliResponse:
179
+ return await request(
180
+ self._pool, Method.DELETE, path,
181
+ version=self.version, timeout_ms=self._timeout_ms,
182
+ headers=headers, params=params,
183
+ )
@@ -0,0 +1,10 @@
1
+ """Protocol orchestration for reclie.
2
+
3
+ Scope for v0.1: HTTP/1.1 and HTTP/2 over plain TCP (http) and TLS (https).
4
+ SSE and WebSocket are deferred; they will land here as ``sse`` / ``ws``
5
+ modules once the HTTP core is solid.
6
+ """
7
+
8
+ from .http import Headers, Method, Params, request
9
+
10
+ __all__ = ["Headers", "Method", "Params", "request"]
@@ -0,0 +1,144 @@
1
+ """HTTP request orchestration.
2
+
3
+ Builds the request descriptor (path + query, flat header list, byte body),
4
+ creates the ``asyncio.Future``, and hands everything to the native engine. The
5
+ engine performs all I/O off the GIL and settles the Future via
6
+ ``loop.call_soon_threadsafe``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import enum
13
+ import json as _json
14
+ from typing import Any, Mapping, Optional, Union
15
+ from urllib.parse import urlencode
16
+
17
+ from ..utils import (
18
+ ConnectionError,
19
+ ProtocolError,
20
+ RecliError,
21
+ RecliResponse,
22
+ TimeoutError,
23
+ TlsError,
24
+ extension,
25
+ )
26
+
27
+ __all__ = ["Method", "Headers", "Params", "request"]
28
+
29
+ # Error-kind ordinals shared with the Zig engine (src/root.zig). 0 == success.
30
+ _ERR_CLASSES = {
31
+ 1: ConnectionError,
32
+ 2: TimeoutError,
33
+ 3: TlsError,
34
+ 4: ProtocolError,
35
+ }
36
+
37
+
38
+ def _settle(
39
+ future: "asyncio.Future[RecliResponse]",
40
+ status: int,
41
+ headers: dict,
42
+ body: bytes,
43
+ err_kind: int,
44
+ err_msg: str,
45
+ ) -> None:
46
+ """Resolve ``future`` from the engine callback.
47
+
48
+ Scheduled via ``loop.call_soon_threadsafe`` from the Zig I/O thread, so this
49
+ always runs on the event-loop thread — the only safe place to touch a Future.
50
+ """
51
+ if future.cancelled():
52
+ return
53
+ if err_kind:
54
+ exc_cls = _ERR_CLASSES.get(err_kind, RecliError)
55
+ future.set_exception(exc_cls(err_msg))
56
+ else:
57
+ future.set_result(RecliResponse(status, headers, body=body))
58
+
59
+ Headers = Mapping[str, str]
60
+ Params = Mapping[str, Union[str, int, float, bool]]
61
+
62
+
63
+ class Method(enum.IntEnum):
64
+ """HTTP methods, matching the Zig ``Method`` enum ordinals."""
65
+
66
+ GET = 0
67
+ POST = 1
68
+ PUT = 2
69
+ PATCH = 3
70
+ DELETE = 4
71
+ HEAD = 5
72
+ OPTIONS = 6
73
+
74
+
75
+ def _build_path(path: str, params: Optional[Params]) -> str:
76
+ if not params:
77
+ return path
78
+ query = urlencode({k: str(v) for k, v in params.items()})
79
+ sep = "&" if "?" in path else "?"
80
+ return f"{path}{sep}{query}"
81
+
82
+
83
+ def _prepare_body(
84
+ headers: Optional[Headers],
85
+ json: Optional[Any],
86
+ data: Optional[bytes],
87
+ ) -> tuple[list[tuple[str, str]], bytes]:
88
+ """Normalize headers + body into a flat header list and raw byte body."""
89
+ if json is not None and data is not None:
90
+ raise ValueError("Pass either `json` or `data`, not both.")
91
+
92
+ header_list: list[tuple[str, str]] = []
93
+ has_content_type = False
94
+ if headers:
95
+ for key, value in headers.items():
96
+ if key.lower() == "content-type":
97
+ has_content_type = True
98
+ header_list.append((key, str(value)))
99
+
100
+ if json is not None:
101
+ body = _json.dumps(json, separators=(",", ":")).encode("utf-8")
102
+ if not has_content_type:
103
+ header_list.append(("Content-Type", "application/json"))
104
+ elif data is not None:
105
+ body = data
106
+ else:
107
+ body = b""
108
+
109
+ return header_list, body
110
+
111
+
112
+ async def request(
113
+ pool: Any,
114
+ method: Method,
115
+ path: str,
116
+ *,
117
+ version: int,
118
+ timeout_ms: int,
119
+ headers: Optional[Headers] = None,
120
+ params: Optional[Params] = None,
121
+ json: Optional[Any] = None,
122
+ data: Optional[bytes] = None,
123
+ ) -> RecliResponse:
124
+ """Issue a single HTTP request and await its response."""
125
+ loop = asyncio.get_running_loop()
126
+ future: asyncio.Future[RecliResponse] = loop.create_future()
127
+
128
+ full_path = _build_path(path, params)
129
+ header_list, body = _prepare_body(headers, json, data)
130
+
131
+ # Non-blocking hand-off: the engine drops the GIL, performs all I/O,
132
+ # then settles `future` via loop.call_soon_threadsafe.
133
+ extension().submit(
134
+ pool,
135
+ int(method),
136
+ full_path,
137
+ header_list,
138
+ body,
139
+ version,
140
+ timeout_ms,
141
+ future,
142
+ loop,
143
+ )
144
+ return await future
@@ -0,0 +1,6 @@
1
+ def main():
2
+ print("Hello from reclie!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,28 @@
1
+ """Internal helpers: extension loader, response types, exceptions.
2
+
3
+ This subpackage aggregates the implementation details so the top-level
4
+ package can re-export the public surface from one place.
5
+ """
6
+
7
+ from ._exceptions import (
8
+ ConnectionError,
9
+ PoolExhaustedError,
10
+ ProtocolError,
11
+ RecliError,
12
+ TimeoutError,
13
+ TlsError,
14
+ )
15
+ from ._ext import extension
16
+ from ._response import RecliHeaders, RecliResponse
17
+
18
+ __all__ = [
19
+ "extension",
20
+ "RecliHeaders",
21
+ "RecliResponse",
22
+ "RecliError",
23
+ "ConnectionError",
24
+ "TimeoutError",
25
+ "TlsError",
26
+ "ProtocolError",
27
+ "PoolExhaustedError",
28
+ ]