fluidattacks_core_http 12.1.0__tar.gz → 12.2.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.
Files changed (19) hide show
  1. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/PKG-INFO +1 -1
  2. fluidattacks_core_http-12.2.0/fluidattacks_core/http/__init__.py +7 -0
  3. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/client.py +54 -1
  4. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/validations.py +1 -1
  5. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/pyproject.toml +1 -1
  6. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/tests/test_client.py +222 -2
  7. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/uv.lock +1 -1
  8. fluidattacks_core_http-12.1.0/build/default.nix +0 -19
  9. fluidattacks_core_http-12.1.0/build/filter.nix +0 -13
  10. fluidattacks_core_http-12.1.0/flake.lock +0 -303
  11. fluidattacks_core_http-12.1.0/flake.nix +0 -29
  12. fluidattacks_core_http-12.1.0/fluidattacks_core/http/__init__.py +0 -5
  13. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/.envrc +0 -0
  14. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/.gitignore +0 -0
  15. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/py.typed +0 -0
  16. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/import-linter.cfg +0 -0
  17. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/mypy.ini +0 -0
  18. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/ruff.toml +0 -0
  19. {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fluidattacks_core_http
3
- Version: 12.1.0
3
+ Version: 12.2.0
4
4
  Summary: Fluid Attacks Core HTTP Library
5
5
  Author-email: Development <development@fluidattacks.com>
6
6
  License: MPL-2.0
@@ -0,0 +1,7 @@
1
+ from .client import (
2
+ MAX_RESPONSE_BYTES,
3
+ ResponseTooLargeError,
4
+ request,
5
+ )
6
+
7
+ __all__ = ["MAX_RESPONSE_BYTES", "ResponseTooLargeError", "request"]
@@ -1,6 +1,7 @@
1
1
  import ipaddress
2
2
  import ssl
3
3
  from collections.abc import Sequence
4
+ from contextvars import ContextVar
4
5
  from typing import (
5
6
  Any,
6
7
  Literal,
@@ -17,6 +18,51 @@ from .validations import (
17
18
 
18
19
  _MAX_REDIRECTS = 10
19
20
  _METHODS_SWITCH_TO_GET = frozenset({301, 302, 303})
21
+ _READ_CHUNK_BYTES = 64 * 1024
22
+
23
+ # Bounds each response so that no single request can exhaust the pod.
24
+ MAX_RESPONSE_BYTES = 64 * 1024 * 1024
25
+
26
+ # aiohttp builds the response object, so the caller's cap cannot be passed to
27
+ # it directly.
28
+ _CAP: ContextVar[int] = ContextVar("max_response_bytes", default=MAX_RESPONSE_BYTES)
29
+
30
+
31
+ class ResponseTooLargeError(aiohttp.ClientPayloadError):
32
+ pass
33
+
34
+
35
+ class BoundedResponse(aiohttp.ClientResponse):
36
+ async def read(self) -> bytes:
37
+ if self._body is None:
38
+ max_response_bytes = _CAP.get()
39
+ try:
40
+ self._reject_oversized_content_length(max_response_bytes)
41
+ self._body = await self._accumulate_capped(max_response_bytes)
42
+ except BaseException:
43
+ self.close()
44
+ raise
45
+ # Deferring to the parent awaits the request writer through internals
46
+ # a subclass cannot name.
47
+ return await super().read()
48
+
49
+ def _reject_oversized_content_length(self, max_response_bytes: int) -> None:
50
+ declared = self.content_length
51
+ if declared is not None and declared > max_response_bytes:
52
+ msg = f"Response declares {declared} bytes, over the {max_response_bytes} limit"
53
+ raise ResponseTooLargeError(msg)
54
+
55
+ async def _accumulate_capped(self, max_response_bytes: int) -> bytes:
56
+ # Counting decoded bytes as they arrive is the only bound that
57
+ # survives a compressed or chunked body, where the declared length
58
+ # says nothing about what decoding produces.
59
+ body = bytearray()
60
+ async for chunk in self.content.iter_chunked(_READ_CHUNK_BYTES):
61
+ body.extend(chunk)
62
+ if len(body) > max_response_bytes:
63
+ msg = f"Response exceeds the {max_response_bytes} byte limit"
64
+ raise ResponseTooLargeError(msg)
65
+ return bytes(body)
20
66
 
21
67
 
22
68
  def get_secure_connector(
@@ -55,6 +101,7 @@ async def request( # noqa: PLR0913
55
101
  enforce_sanitization: bool = False,
56
102
  headers: dict[str, str] | None = None,
57
103
  json: Any | None = None, # noqa: ANN401
104
+ max_response_bytes: int = MAX_RESPONSE_BYTES,
58
105
  ports: list[int] | None = None,
59
106
  schemes: list[str] | None = None,
60
107
  timeout: int = 10, # noqa: ASYNC109
@@ -77,6 +124,7 @@ async def request( # noqa: PLR0913
77
124
  connection = connector(
78
125
  ssl=ssl.create_default_context(cafile=certifi.where()),
79
126
  )
127
+ _CAP.set(max_response_bytes)
80
128
  if allow_redirects is True:
81
129
  return await _request_with_validated_redirects(
82
130
  connection=connection,
@@ -98,6 +146,7 @@ async def request( # noqa: PLR0913
98
146
  aiohttp.ClientSession(
99
147
  connector=connection,
100
148
  headers=headers,
149
+ response_class=BoundedResponse,
101
150
  ) as session,
102
151
  session.request(
103
152
  method,
@@ -130,7 +179,11 @@ async def _request_with_validated_redirects( # noqa: PLR0913
130
179
  current_url = initial_url
131
180
  current_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = initial_method
132
181
  current_json: Any = json
133
- async with aiohttp.ClientSession(connector=connection, headers=headers) as session:
182
+ async with aiohttp.ClientSession(
183
+ connector=connection,
184
+ headers=headers,
185
+ response_class=BoundedResponse,
186
+ ) as session:
134
187
  for _ in range(_MAX_REDIRECTS + 1):
135
188
  async with session.request(
136
189
  current_method,
@@ -10,7 +10,7 @@ class HTTPValidationError(Exception):
10
10
 
11
11
 
12
12
  def validate_scheme(scheme: str | None, schemes: list[str]) -> None:
13
- if scheme and scheme not in schemes:
13
+ if scheme not in schemes:
14
14
  msg = f"Only allowed schemes are {', '.join(schemes)}"
15
15
  raise HTTPValidationError(msg)
16
16
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fluidattacks_core_http"
3
- version = "12.1.0"
3
+ version = "12.2.0"
4
4
  description = "Fluid Attacks Core HTTP Library"
5
5
  authors = [{ name = "Development", email = "development@fluidattacks.com" }]
6
6
  license = { text = "MPL-2.0" }
@@ -1,5 +1,7 @@
1
1
  """Tests for fluidattacks_core.http.client.request."""
2
2
 
3
+ import asyncio
4
+ import gzip
3
5
  from collections.abc import Awaitable, Callable
4
6
 
5
7
  import aiohttp
@@ -7,10 +9,12 @@ import pytest
7
9
  from aiohttp import web
8
10
  from aiohttp.test_utils import TestServer
9
11
 
10
- from fluidattacks_core.http import request
11
- from fluidattacks_core.http.validations import HTTPValidationError
12
+ from fluidattacks_core.http import ResponseTooLargeError, request
13
+ from fluidattacks_core.http.validations import HTTPValidationError, validate_url
12
14
 
13
15
  RouteHandler = Callable[[web.Request], Awaitable[web.StreamResponse]]
16
+ _CAP = 64 * 1024
17
+ _MAX_HOPS = 11
14
18
 
15
19
 
16
20
  async def _build_server(routes: dict[str, RouteHandler]) -> TestServer:
@@ -237,3 +241,219 @@ async def test_exceeding_max_redirects_raises_too_many_redirects() -> None:
237
241
  )
238
242
  finally:
239
243
  await server.close()
244
+
245
+
246
+ def _gzip_bomb(decompressed_bytes: int) -> bytes:
247
+ return gzip.compress(b"\x00" * decompressed_bytes, compresslevel=9)
248
+
249
+
250
+ async def _build_body_server(body: bytes, headers: dict[str, str]) -> TestServer:
251
+ async def handler(_req: web.Request) -> web.Response:
252
+ return web.Response(body=body, headers=headers)
253
+
254
+ return await _build_server({"/body": handler})
255
+
256
+
257
+ async def _capped_request(
258
+ url: str,
259
+ *,
260
+ max_response_bytes: int,
261
+ allow_redirects: bool | None = None,
262
+ ) -> aiohttp.ClientResponse:
263
+ return await request(
264
+ url,
265
+ method="GET",
266
+ allow_local_network=True,
267
+ allow_localhost=True,
268
+ allow_redirects=allow_redirects,
269
+ dns_rebind_protection=False,
270
+ max_response_bytes=max_response_bytes,
271
+ )
272
+
273
+
274
+ @pytest.mark.asyncio
275
+ async def test_compressed_body_over_cap_is_rejected() -> None:
276
+ bomb = _gzip_bomb(8 * 1024 * 1024)
277
+ server = await _build_body_server(
278
+ bomb,
279
+ {"Content-Encoding": "gzip", "Content-Type": "application/json"},
280
+ )
281
+ try:
282
+ assert len(bomb) < 1024 * 1024
283
+ with pytest.raises(ResponseTooLargeError):
284
+ await _capped_request(
285
+ _make_server_url(server, "/body"),
286
+ max_response_bytes=1024 * 1024,
287
+ )
288
+ finally:
289
+ await server.close()
290
+
291
+
292
+ @pytest.mark.asyncio
293
+ async def test_body_within_cap_is_still_returned() -> None:
294
+ server = await _build_body_server(b"payload", {})
295
+ try:
296
+ response = await _capped_request(
297
+ _make_server_url(server, "/body"),
298
+ max_response_bytes=1024 * 1024,
299
+ )
300
+ assert await response.text() == "payload"
301
+ finally:
302
+ await server.close()
303
+
304
+
305
+ @pytest.mark.asyncio
306
+ async def test_cap_rejects_an_oversized_body_after_a_redirect() -> None:
307
+ bomb = _gzip_bomb(8 * 1024 * 1024)
308
+
309
+ async def handle_start(_req: web.Request) -> web.Response:
310
+ return web.Response(status=302, headers={"Location": "/final"})
311
+
312
+ async def handle_final(_req: web.Request) -> web.Response:
313
+ return web.Response(
314
+ body=bomb,
315
+ headers={"Content-Encoding": "gzip", "Content-Type": "application/json"},
316
+ )
317
+
318
+ server = await _build_server({"/start": handle_start, "/final": handle_final})
319
+ try:
320
+ with pytest.raises(ResponseTooLargeError):
321
+ await _capped_request(
322
+ _make_server_url(server, "/start"),
323
+ max_response_bytes=1024 * 1024,
324
+ allow_redirects=True,
325
+ )
326
+ finally:
327
+ await server.close()
328
+
329
+
330
+ @pytest.mark.asyncio
331
+ async def test_declared_length_over_cap_is_rejected() -> None:
332
+ server = await _build_body_server(b"a" * (2 * 1024 * 1024), {})
333
+ try:
334
+ with pytest.raises(ResponseTooLargeError, match="declares"):
335
+ await _capped_request(
336
+ _make_server_url(server, "/body"),
337
+ max_response_bytes=1024 * 1024,
338
+ )
339
+ finally:
340
+ await server.close()
341
+
342
+
343
+ @pytest.mark.parametrize(
344
+ "url",
345
+ [
346
+ "//evil.example/jwks",
347
+ "//evil.example:8080/jwks",
348
+ "evil.example/jwks",
349
+ "evil.example:8080/jwks",
350
+ ],
351
+ )
352
+ def test_schemeless_url_is_rejected_when_schemes_are_restricted(url: str) -> None:
353
+ with pytest.raises(HTTPValidationError):
354
+ validate_url(
355
+ url,
356
+ ascii_only=False,
357
+ enforce_sanitization=False,
358
+ ports=[],
359
+ schemes=["https"],
360
+ )
361
+
362
+
363
+ def test_allowed_scheme_still_passes() -> None:
364
+ validate_url(
365
+ "https://good.example/jwks",
366
+ ascii_only=False,
367
+ enforce_sanitization=False,
368
+ ports=[],
369
+ schemes=["https"],
370
+ )
371
+
372
+
373
+ @pytest.mark.asyncio
374
+ async def test_relative_redirect_keeps_the_scheme_of_its_base() -> None:
375
+ async def handle_start(_req: web.Request) -> web.Response:
376
+ return web.Response(status=302, headers={"Location": "/final"})
377
+
378
+ async def handle_final(_req: web.Request) -> web.Response:
379
+ return web.Response(status=200, text="ok")
380
+
381
+ server = await _build_server({"/start": handle_start, "/final": handle_final})
382
+ try:
383
+ response = await _permissive_request(
384
+ _make_server_url(server, "/start"),
385
+ allow_redirects=True,
386
+ schemes=["http"],
387
+ )
388
+ assert response.status == 200
389
+ assert await response.text() == "ok"
390
+ finally:
391
+ await server.close()
392
+
393
+ @pytest.mark.asyncio
394
+ @pytest.mark.parametrize("compressed", [False, True])
395
+ async def test_cap_admits_a_body_of_exactly_the_limit(compressed: bool) -> None:
396
+ body = _gzip_bomb(_CAP) if compressed else b"a" * _CAP
397
+ headers = {"Content-Encoding": "gzip"} if compressed else {}
398
+ server = await _build_body_server(body, headers)
399
+ try:
400
+ response = await _capped_request(
401
+ _make_server_url(server, "/body"), max_response_bytes=_CAP
402
+ )
403
+ assert len(await response.text()) == _CAP
404
+ finally:
405
+ await server.close()
406
+
407
+
408
+ @pytest.mark.asyncio
409
+ async def test_cap_bounds_each_hop_rather_than_the_whole_chain() -> None:
410
+ # The accepted multiplier: every hop may serve just under the cap, so a
411
+ # full chain moves _MAX_HOPS times the limit and still succeeds.
412
+ body = b"a" * (_CAP - 1)
413
+
414
+ async def handle_hop(req: web.Request) -> web.Response:
415
+ hop = int(req.query.get("n", "0"))
416
+ if hop >= _MAX_HOPS - 1:
417
+ return web.Response(body=body)
418
+ return web.Response(
419
+ status=302, body=body, headers={"Location": f"/hop?n={hop + 1}"}
420
+ )
421
+
422
+ server = await _build_server({"/hop": handle_hop})
423
+ try:
424
+ response = await _capped_request(
425
+ _make_server_url(server, "/hop?n=0"),
426
+ max_response_bytes=_CAP,
427
+ allow_redirects=True,
428
+ )
429
+ assert response.status == 200
430
+ assert len(await response.text()) == _CAP - 1
431
+ finally:
432
+ await server.close()
433
+
434
+ def test_response_too_large_stays_catchable_as_a_client_error() -> None:
435
+ # Callers degrade gracefully on transport failures; narrowing this base
436
+ # converts their soft fallbacks into client-facing errors, and in one
437
+ # fire-and-forget path drops the log entirely.
438
+ assert issubclass(ResponseTooLargeError, aiohttp.ClientError)
439
+
440
+
441
+ @pytest.mark.asyncio
442
+ async def test_concurrent_requests_do_not_share_a_cap() -> None:
443
+ # The cap reaches the response through context, so overlapping requests
444
+ # must not observe each other's limit.
445
+ server = await _build_body_server(
446
+ _gzip_bomb(4 * 1024 * 1024), {"Content-Encoding": "gzip"}
447
+ )
448
+ url = _make_server_url(server, "/body")
449
+ try:
450
+ tight, generous = await asyncio.gather(
451
+ _capped_request(url, max_response_bytes=1024 * 1024),
452
+ _capped_request(url, max_response_bytes=32 * 1024 * 1024),
453
+ return_exceptions=True,
454
+ )
455
+ assert isinstance(tight, ResponseTooLargeError)
456
+ assert not isinstance(generous, BaseException)
457
+ assert len(await generous.text()) == 4 * 1024 * 1024
458
+ finally:
459
+ await server.close()
@@ -217,7 +217,7 @@ wheels = [
217
217
 
218
218
  [[package]]
219
219
  name = "fluidattacks-core-http"
220
- version = "12.1.0"
220
+ version = "12.2.0"
221
221
  source = { editable = "." }
222
222
  dependencies = [
223
223
  { name = "aiohttp" },
@@ -1,19 +0,0 @@
1
- { src, workspace_root }:
2
- {
3
- inherit src workspace_root;
4
- root_path = "develops/fluidattacks-core/python/http";
5
- module_name = "fluidattacks_core";
6
- pypi_token = {
7
- method = "oidc";
8
- };
9
- override = {
10
- checks = {
11
- basedpyright = false;
12
- };
13
- configs = {
14
- import_linter = ../import-linter.cfg;
15
- mypy = ../mypy.ini;
16
- ruff = ../ruff.toml;
17
- };
18
- };
19
- }
@@ -1,13 +0,0 @@
1
- path_filter: src:
2
- path_filter {
3
- root = src;
4
- include = [
5
- "fluidattacks_core"
6
- "tests"
7
- "import-linter.cfg"
8
- "mypy.ini"
9
- "pyproject.toml"
10
- "ruff.toml"
11
- "uv.lock"
12
- ];
13
- }
@@ -1,303 +0,0 @@
1
- {
2
- "nodes": {
3
- "flake-parts": {
4
- "inputs": {
5
- "nixpkgs-lib": "nixpkgs-lib"
6
- },
7
- "locked": {
8
- "lastModified": 1762810396,
9
- "narHash": "sha256-dxFVgQPG+R72dkhXTtqUm7KpxElw3u6E+YlQ2WaDgt8=",
10
- "owner": "hercules-ci",
11
- "repo": "flake-parts",
12
- "rev": "0bdadb1b265fb4143a75bd1ec7d8c915898a9923",
13
- "type": "github"
14
- },
15
- "original": {
16
- "owner": "hercules-ci",
17
- "repo": "flake-parts",
18
- "type": "github"
19
- }
20
- },
21
- "nix_filter": {
22
- "locked": {
23
- "lastModified": 1757882181,
24
- "narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
25
- "owner": "numtide",
26
- "repo": "nix-filter",
27
- "rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
28
- "type": "github"
29
- },
30
- "original": {
31
- "owner": "numtide",
32
- "repo": "nix-filter",
33
- "type": "github"
34
- }
35
- },
36
- "nixpkgs": {
37
- "locked": {
38
- "lastModified": 1758690382,
39
- "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
40
- "owner": "nixos",
41
- "repo": "nixpkgs",
42
- "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
43
- "type": "github"
44
- },
45
- "original": {
46
- "owner": "nixos",
47
- "ref": "nixos-unstable",
48
- "repo": "nixpkgs",
49
- "type": "github"
50
- }
51
- },
52
- "nixpkgs-lib": {
53
- "locked": {
54
- "lastModified": 1761765539,
55
- "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
56
- "owner": "nix-community",
57
- "repo": "nixpkgs.lib",
58
- "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
59
- "type": "github"
60
- },
61
- "original": {
62
- "owner": "nix-community",
63
- "repo": "nixpkgs.lib",
64
- "type": "github"
65
- }
66
- },
67
- "nixpkgs_2": {
68
- "locked": {
69
- "lastModified": 1758690382,
70
- "narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
71
- "owner": "nixos",
72
- "repo": "nixpkgs",
73
- "rev": "e643668fd71b949c53f8626614b21ff71a07379d",
74
- "type": "github"
75
- },
76
- "original": {
77
- "owner": "nixos",
78
- "ref": "nixos-unstable",
79
- "repo": "nixpkgs",
80
- "type": "github"
81
- }
82
- },
83
- "nixpkgs_3": {
84
- "locked": {
85
- "lastModified": 1761373498,
86
- "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=",
87
- "owner": "nixos",
88
- "repo": "nixpkgs",
89
- "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce",
90
- "type": "github"
91
- },
92
- "original": {
93
- "owner": "nixos",
94
- "ref": "nixos-unstable",
95
- "repo": "nixpkgs",
96
- "type": "github"
97
- }
98
- },
99
- "nixpkgs_flake": {
100
- "locked": {
101
- "lastModified": 1778253243,
102
- "narHash": "sha256-KkH2QvU6iRLDTGGEwH5jnMqHWj4zMTuHxi1S1zdr01w=",
103
- "owner": "nixos",
104
- "repo": "nixpkgs",
105
- "rev": "4fcda7103530b23c5ca44286344912082aad4220",
106
- "type": "github"
107
- },
108
- "original": {
109
- "owner": "nixos",
110
- "repo": "nixpkgs",
111
- "type": "github"
112
- }
113
- },
114
- "pyproject-build-systems": {
115
- "inputs": {
116
- "nixpkgs": "nixpkgs",
117
- "pyproject-nix": "pyproject-nix",
118
- "uv2nix": "uv2nix"
119
- },
120
- "locked": {
121
- "lastModified": 1761781027,
122
- "narHash": "sha256-YDvxPAm2WnxrznRqWwHLjryBGG5Ey1ATEJXrON+TWt8=",
123
- "owner": "pyproject-nix",
124
- "repo": "build-system-pkgs",
125
- "rev": "795a980d25301e5133eca37adae37283ec3c8e66",
126
- "type": "github"
127
- },
128
- "original": {
129
- "owner": "pyproject-nix",
130
- "repo": "build-system-pkgs",
131
- "type": "github"
132
- }
133
- },
134
- "pyproject-nix": {
135
- "inputs": {
136
- "nixpkgs": [
137
- "python_builder",
138
- "pyproject-build-systems",
139
- "nixpkgs"
140
- ]
141
- },
142
- "locked": {
143
- "lastModified": 1758265079,
144
- "narHash": "sha256-amLaLNwKSZPShQHzfgmc/9o76dU8xzN0743dWgvYlr8=",
145
- "owner": "nix-community",
146
- "repo": "pyproject.nix",
147
- "rev": "02e9418fd4af638447dca4b17b1280da95527fc9",
148
- "type": "github"
149
- },
150
- "original": {
151
- "owner": "nix-community",
152
- "repo": "pyproject.nix",
153
- "type": "github"
154
- }
155
- },
156
- "pyproject-nix_2": {
157
- "inputs": {
158
- "nixpkgs": "nixpkgs_2"
159
- },
160
- "locked": {
161
- "lastModified": 1762427963,
162
- "narHash": "sha256-CkPlAbIQ87wmjy5qHibfzk4DmMGBNqFer+lLfXjpP5M=",
163
- "owner": "pyproject-nix",
164
- "repo": "pyproject.nix",
165
- "rev": "4540ea004e04fcd12dd2738d51383d10f956f7b9",
166
- "type": "github"
167
- },
168
- "original": {
169
- "owner": "pyproject-nix",
170
- "repo": "pyproject.nix",
171
- "type": "github"
172
- }
173
- },
174
- "pyproject-nix_3": {
175
- "inputs": {
176
- "nixpkgs": [
177
- "python_builder",
178
- "uv2nix",
179
- "nixpkgs"
180
- ]
181
- },
182
- "locked": {
183
- "lastModified": 1760402624,
184
- "narHash": "sha256-jF6UKLs2uGc2rtved8Vrt58oTWjTQoAssuYs/0578Z4=",
185
- "owner": "pyproject-nix",
186
- "repo": "pyproject.nix",
187
- "rev": "84c4ea102127c77058ea1ed7be7300261fafc7d2",
188
- "type": "github"
189
- },
190
- "original": {
191
- "owner": "pyproject-nix",
192
- "repo": "pyproject.nix",
193
- "type": "github"
194
- }
195
- },
196
- "python_builder": {
197
- "inputs": {
198
- "nix_filter": "nix_filter",
199
- "nixpkgs_flake": "nixpkgs_flake",
200
- "pyproject-build-systems": "pyproject-build-systems",
201
- "pyproject-nix": "pyproject-nix_2",
202
- "shell-helpers": "shell-helpers",
203
- "uv2nix": "uv2nix_2"
204
- },
205
- "locked": {
206
- "dir": "common/utils/python-builder",
207
- "lastModified": 1787078362,
208
- "narHash": "sha256-HYL7Rn6SuNjYxkt/PwFLksZjsauCiY+ENbOWryLsIlw=",
209
- "ref": "refs/heads/trunk",
210
- "rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
211
- "shallow": true,
212
- "type": "git",
213
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
214
- },
215
- "original": {
216
- "dir": "common/utils/python-builder",
217
- "rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
218
- "shallow": true,
219
- "type": "git",
220
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
221
- }
222
- },
223
- "root": {
224
- "inputs": {
225
- "python_builder": "python_builder"
226
- }
227
- },
228
- "shell-helpers": {
229
- "inputs": {
230
- "flake-parts": "flake-parts",
231
- "nixpkgs": [
232
- "python_builder",
233
- "nixpkgs_flake"
234
- ]
235
- },
236
- "locked": {
237
- "dir": "common/utils/shell-helpers",
238
- "lastModified": 1762806066,
239
- "narHash": "sha256-Wz99Fl7SpPU1NkTHJ36XOo6DjOEAMXCw3ThRABOS2Hs=",
240
- "ref": "refs/heads/trunk",
241
- "rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
242
- "shallow": true,
243
- "type": "git",
244
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
245
- },
246
- "original": {
247
- "dir": "common/utils/shell-helpers",
248
- "rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
249
- "shallow": true,
250
- "type": "git",
251
- "url": "ssh://git@gitlab.com/fluidattacks/universe"
252
- }
253
- },
254
- "uv2nix": {
255
- "inputs": {
256
- "nixpkgs": [
257
- "python_builder",
258
- "pyproject-build-systems",
259
- "nixpkgs"
260
- ],
261
- "pyproject-nix": [
262
- "python_builder",
263
- "pyproject-build-systems",
264
- "pyproject-nix"
265
- ]
266
- },
267
- "locked": {
268
- "lastModified": 1758933732,
269
- "narHash": "sha256-HAmm1GBS1myZCFuog0DC2ZLaynvZtiUI2Crmo+cdQI0=",
270
- "owner": "pyproject-nix",
271
- "repo": "uv2nix",
272
- "rev": "273ce18f913d8559e0d04f820d724308966d7c4d",
273
- "type": "github"
274
- },
275
- "original": {
276
- "owner": "pyproject-nix",
277
- "repo": "uv2nix",
278
- "type": "github"
279
- }
280
- },
281
- "uv2nix_2": {
282
- "inputs": {
283
- "nixpkgs": "nixpkgs_3",
284
- "pyproject-nix": "pyproject-nix_3"
285
- },
286
- "locked": {
287
- "lastModified": 1761872265,
288
- "narHash": "sha256-i25GRgp2vUOebY70L3NTAgkd+Pr1hnn5xM3qHxH0ONU=",
289
- "owner": "pyproject-nix",
290
- "repo": "uv2nix",
291
- "rev": "74dfb62871be152ad3673b143b0cc56105a4f3c5",
292
- "type": "github"
293
- },
294
- "original": {
295
- "owner": "pyproject-nix",
296
- "repo": "uv2nix",
297
- "type": "github"
298
- }
299
- }
300
- },
301
- "root": "root",
302
- "version": 7
303
- }
@@ -1,29 +0,0 @@
1
- {
2
- description = "Fluid Attacks Core http package.";
3
-
4
- inputs = {
5
- python_builder = {
6
- url = "git+ssh://git@gitlab.com/fluidattacks/universe?shallow=1&rev=09fd25a13309eb5e3b5b2a061bb340df3ae5d281&dir=common/utils/python-builder";
7
- };
8
- };
9
-
10
- outputs =
11
- { self, ... }@inputs:
12
- let
13
- build_args =
14
- {
15
- system,
16
- python_version,
17
- nixpkgs,
18
- builders,
19
- scripts,
20
- }:
21
- import ./build {
22
- src = import ./build/filter.nix nixpkgs.nix-filter self;
23
- workspace_root = ./.;
24
- };
25
- in
26
- {
27
- packages = inputs.python_builder.outputs.build build_args;
28
- };
29
- }
@@ -1,5 +0,0 @@
1
- from .client import (
2
- request,
3
- )
4
-
5
- __all__ = ["request"]