socketsecurity 2.3.0__tar.gz → 2.3.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.
Files changed (137) hide show
  1. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/CHANGELOG.md +27 -0
  2. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/PKG-INFO +3 -1
  3. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/pyproject.toml +3 -1
  4. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/__init__.py +1 -1
  5. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/__init__.py +129 -1
  6. socketsecurity-2.3.1/tests/core/test_facts_compression.py +137 -0
  7. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/uv.lock +108 -2
  8. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/CODEOWNERS +0 -0
  9. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
  10. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
  11. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
  12. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/dependabot.yml +0 -0
  14. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/workflows/dependabot-review.yml +0 -0
  15. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/workflows/docker-stable.yml +0 -0
  16. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/workflows/e2e-test.yml +0 -0
  17. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/workflows/pr-preview.yml +0 -0
  18. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/workflows/python-tests.yml +0 -0
  19. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/workflows/release.yml +0 -0
  20. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/workflows/version-check.yml +0 -0
  21. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.github/zizmor.yml +0 -0
  22. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.gitignore +0 -0
  23. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.hooks/sync_version.py +0 -0
  24. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.pre-commit-config.yaml +0 -0
  25. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/.python-version +0 -0
  26. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/Dockerfile +0 -0
  27. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/LICENSE +0 -0
  28. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/Makefile +0 -0
  29. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/README.md +0 -0
  30. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/docs/ci-cd.md +0 -0
  31. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/docs/cli-reference.md +0 -0
  32. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/docs/development.md +0 -0
  33. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/docs/troubleshooting.md +0 -0
  34. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/examples/config/sarif-dashboard-parity.json +0 -0
  35. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/examples/config/sarif-dashboard-parity.toml +0 -0
  36. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/examples/config/sarif-diff-ci-cd.json +0 -0
  37. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/examples/config/sarif-diff-ci-cd.toml +0 -0
  38. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/examples/config/sarif-instance-detail.json +0 -0
  39. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/examples/config/sarif-instance-detail.toml +0 -0
  40. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/instructions/gitlab-commit-status/uat.md +0 -0
  41. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/pytest.ini +0 -0
  42. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/scripts/build_container.sh +0 -0
  43. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/scripts/build_container_flexible.sh +0 -0
  44. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/scripts/deploy-test-docker.sh +0 -0
  45. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/scripts/deploy-test-pypi.sh +0 -0
  46. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/scripts/docker-entrypoint.sh +0 -0
  47. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/scripts/run.sh +0 -0
  48. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/session.md +0 -0
  49. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socket.yml +0 -0
  50. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/config.py +0 -0
  51. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/alert_selection.py +0 -0
  52. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/classes.py +0 -0
  53. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/cli_client.py +0 -0
  54. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/exceptions.py +0 -0
  55. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/git_interface.py +0 -0
  56. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/helper/__init__.py +0 -0
  57. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
  58. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/lazy_file_loader.py +0 -0
  59. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/logging.py +0 -0
  60. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/messages.py +0 -0
  61. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/resource_utils.py +0 -0
  62. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/scm/__init__.py +0 -0
  63. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/scm/base.py +0 -0
  64. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/scm/client.py +0 -0
  65. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/scm/github.py +0 -0
  66. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/scm/gitlab.py +0 -0
  67. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/scm_comments.py +0 -0
  68. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/socket_config.py +0 -0
  69. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/tools/reachability.py +0 -0
  70. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/core/utils.py +0 -0
  71. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/fossa_compat.py +0 -0
  72. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/output.py +0 -0
  73. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/__init__.py +0 -0
  74. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/base.py +0 -0
  75. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/formatters/__init__.py +0 -0
  76. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/formatters/slack.py +0 -0
  77. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/jira.py +0 -0
  78. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/manager.py +0 -0
  79. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/slack.py +0 -0
  80. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/teams.py +0 -0
  81. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/plugins/webhook.py +0 -0
  82. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/socketsecurity/socketcli.py +0 -0
  83. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/__init__.py +0 -0
  84. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/conftest.py +0 -0
  85. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/create_diff_input.json +0 -0
  86. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/test_diff_alerts.py +0 -0
  87. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/test_diff_generation.py +0 -0
  88. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/test_has_manifest_files.py +0 -0
  89. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/test_package_and_alerts.py +0 -0
  90. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/test_sdk_methods.py +0 -0
  91. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/core/test_supporting_methods.py +0 -0
  92. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/create_response.json +0 -0
  93. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/diff/stream_diff.json +0 -0
  94. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
  95. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/head_scan/metadata.json +0 -0
  96. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
  97. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
  98. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/new_scan/metadata.json +0 -0
  99. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
  100. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/repos/repo_info_error.json +0 -0
  101. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/repos/repo_info_no_head.json +0 -0
  102. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/repos/repo_info_success.json +0 -0
  103. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/data/settings/security-policy.json +0 -0
  104. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/fixtures/simple-npm/index.js +0 -0
  105. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/fixtures/simple-npm/package.json +0 -0
  106. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
  107. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/validate-gitlab.sh +0 -0
  108. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/validate-json.sh +0 -0
  109. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/validate-reachability.sh +0 -0
  110. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/validate-sarif.sh +0 -0
  111. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/e2e/validate-scan.sh +0 -0
  112. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/fixtures/fossa/README.md +0 -0
  113. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-analyze-empty.json +0 -0
  114. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-analyze-populated.json +0 -0
  115. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-sbom-empty-deep.json +0 -0
  116. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/fixtures/fossa/fossa-sbom-populated.json +0 -0
  117. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/__init__.py +0 -0
  118. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_alert_selection.py +0 -0
  119. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_cli_config.py +0 -0
  120. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_client.py +0 -0
  121. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_config.py +0 -0
  122. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_dependency_overview.py +0 -0
  123. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_disable_ignore.py +0 -0
  124. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_fossa_compat.py +0 -0
  125. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_fossa_parity.py +0 -0
  126. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_gitlab_auth.py +0 -0
  127. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_gitlab_auth_fallback.py +0 -0
  128. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_gitlab_commit_status.py +0 -0
  129. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_gitlab_format.py +0 -0
  130. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_ignore_telemetry_filtering.py +0 -0
  131. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_output.py +0 -0
  132. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_slack_plugin.py +0 -0
  133. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/tests/unit/test_socketcli.py +0 -0
  134. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/workflows/bitbucket-pipelines.yml +0 -0
  135. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/workflows/buildkite.yml +0 -0
  136. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/workflows/github-actions.yml +0 -0
  137. {socketsecurity-2.3.0 → socketsecurity-2.3.1}/workflows/gitlab-ci.yml +0 -0
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.1
4
+
5
+ ### New: brotli-compressed `.socket.facts.json` upload
6
+
7
+ The reachability facts file (`.socket.facts.json`) is now brotli-compressed before it is
8
+ uploaded as part of a full scan. The Socket API transparently decompresses any multipart
9
+ part named exactly `.socket.facts.json.br` and stores it as plain `.socket.facts.json`, so
10
+ the stored result is unchanged — but the on-the-wire payload shrinks dramatically (a
11
+ ~262 MB facts file compresses to roughly 15–30 MB).
12
+
13
+ This fixes large tier‑1 reachability scans that previously failed when the uncompressed
14
+ facts file exceeded the API's per‑file upload size cap (surfaced to the CLI as an HTTP
15
+ 4xx/“502”, leaving the scan stuck with no report).
16
+
17
+ Details:
18
+
19
+ - Compression happens at the upload boundary (`Core.create_full_scan`); the file on disk is
20
+ left untouched, so local consumers (SARIF/JSON output, tier‑1 finalize, alert selection)
21
+ continue to read the plain `.socket.facts.json`.
22
+ - Only a file whose basename is exactly `.socket.facts.json` is compressed (the API matches
23
+ that exact name). A custom `--reach-output-file` name is uploaded uncompressed, as before.
24
+ - Empty baseline-scan placeholder files are not compressed.
25
+ - Compression never blocks an upload: if it fails for any reason it falls back to uploading
26
+ the plain file, and a partially-written `.socket.facts.json.br` is removed rather than
27
+ left behind in the target directory.
28
+ - Adds a `brotli` (CPython) / `brotlicffi` (PyPy) dependency.
29
+
3
30
  ## 2.3.0
4
31
 
5
32
  ### New: `--exit-code-on-api-error`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socketsecurity
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: Socket Security CLI for CI/CD
5
5
  Project-URL: Homepage, https://socket.dev
6
6
  Author-email: Douglas Coburn <douglas@socket.dev>
@@ -33,6 +33,8 @@ Classifier: Intended Audience :: Developers
33
33
  Classifier: Programming Language :: Python :: 3.11
34
34
  Classifier: Programming Language :: Python :: 3.12
35
35
  Requires-Python: >=3.11
36
+ Requires-Dist: brotli>=1.0.9; platform_python_implementation == 'CPython'
37
+ Requires-Dist: brotlicffi>=1.0.9; platform_python_implementation != 'CPython'
36
38
  Requires-Dist: bs4>=0.0.2
37
39
  Requires-Dist: gitpython
38
40
  Requires-Dist: markdown>=3.10
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "socketsecurity"
9
- version = "2.3.0"
9
+ version = "2.3.1"
10
10
  requires-python = ">= 3.11"
11
11
  license = {"file" = "LICENSE"}
12
12
  dependencies = [
@@ -19,6 +19,8 @@ dependencies = [
19
19
  "socketdev>=3.0.33,<4.0.0",
20
20
  "bs4>=0.0.2",
21
21
  "markdown>=3.10",
22
+ "brotli>=1.0.9; platform_python_implementation == 'CPython'",
23
+ "brotlicffi>=1.0.9; platform_python_implementation != 'CPython'",
22
24
  ]
23
25
  readme = "README.md"
24
26
  description = "Socket Security CLI for CI/CD"
@@ -1,3 +1,3 @@
1
1
  __author__ = 'socket.dev'
2
- __version__ = '2.3.0'
2
+ __version__ = '2.3.1'
3
3
  USER_AGENT = f'SocketPythonCLI/{__version__}'
@@ -51,6 +51,26 @@ _ALERT_TYPE_TITLE_OVERRIDES = {
51
51
 
52
52
  _HUMANIZE_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
53
53
 
54
+ # Reachability facts-file upload compression.
55
+ #
56
+ # The Socket full-scan endpoint transparently brotli-decompresses any multipart part
57
+ # whose basename is exactly ``.socket.facts.json.br`` and stores it as plain
58
+ # ``.socket.facts.json``. Compressing the facts file on upload keeps it well under the
59
+ # server's per-file size cap (a ~262 MB facts file compresses to roughly 15-30 MB),
60
+ # which is required for large reachability (tier 1) scans to succeed.
61
+ #
62
+ # The server matches the *exact* name ``.socket.facts.json.br``, so we only compress
63
+ # files whose basename is exactly ``.socket.facts.json`` (a custom ``--reach-output-file``
64
+ # name would not be decompressed server-side, so it is left as a plain upload).
65
+ SOCKET_FACTS_FILENAME = ".socket.facts.json"
66
+ SOCKET_FACTS_BROTLI_FILENAME = ".socket.facts.json.br"
67
+ # Brotli quality (0-11); 5 is a good speed/ratio tradeoff for large JSON payloads.
68
+ SOCKET_FACTS_BROTLI_QUALITY = 5
69
+ # Largest brotli window (2**24 bytes); improves the ratio on large facts files.
70
+ SOCKET_FACTS_BROTLI_LGWIN = 24
71
+ # Stream the facts file in 1 MiB chunks so large files aren't held fully in memory.
72
+ SOCKET_FACTS_BROTLI_CHUNK_SIZE = 1024 * 1024
73
+
54
74
 
55
75
  def _humanize_alert_type(alert_type: str) -> str:
56
76
  """Convert a camelCase/PascalCase alert type into a Title-Cased label.
@@ -544,6 +564,102 @@ class Core:
544
564
  log.debug(f"Unable to finalize tier 1 scan: {e}")
545
565
  return False
546
566
 
567
+ @staticmethod
568
+ def _compress_facts_file(source_path: str) -> str:
569
+ """Brotli-compress a ``.socket.facts.json`` file to a sibling ``.socket.facts.json.br``.
570
+
571
+ The source is streamed in chunks so a large facts file (hundreds of MB) never has
572
+ to be held in memory at once. The compressed file is written next to the source so
573
+ that the multipart key the SDK derives keeps the same directory prefix, only with a
574
+ ``.br`` basename. Any existing ``.socket.facts.json.br`` sibling is overwritten, and a
575
+ partially-written output is removed if compression fails part-way through (e.g. the
576
+ disk fills up mid-stream) so no orphaned ``.br`` is left in the target directory.
577
+
578
+ Args:
579
+ source_path: Path to the plain ``.socket.facts.json`` file.
580
+
581
+ Returns:
582
+ Path to the compressed sibling file.
583
+ """
584
+ # Imported lazily so the dependency is only needed when actually uploading a facts
585
+ # file. brotlicffi is the API-compatible fallback used on PyPy / non-CPython runtimes.
586
+ try:
587
+ import brotli
588
+ except ImportError:
589
+ import brotlicffi as brotli
590
+
591
+ target_path = os.path.join(os.path.dirname(source_path), SOCKET_FACTS_BROTLI_FILENAME)
592
+ compressor = brotli.Compressor(
593
+ quality=SOCKET_FACTS_BROTLI_QUALITY,
594
+ lgwin=SOCKET_FACTS_BROTLI_LGWIN,
595
+ )
596
+ try:
597
+ with open(source_path, "rb") as src, open(target_path, "wb") as dst:
598
+ while True:
599
+ chunk = src.read(SOCKET_FACTS_BROTLI_CHUNK_SIZE)
600
+ if not chunk:
601
+ break
602
+ compressed = compressor.process(chunk)
603
+ if compressed:
604
+ dst.write(compressed)
605
+ dst.write(compressor.finish())
606
+ except BaseException:
607
+ # Don't leave a half-written .br behind for the caller to miss (it only tracks
608
+ # the path for cleanup once this returns). Remove it, then re-raise so the caller
609
+ # falls back to uploading the plain file.
610
+ try:
611
+ os.unlink(target_path)
612
+ except OSError:
613
+ pass
614
+ raise
615
+ return target_path
616
+
617
+ def _compress_facts_files_for_upload(self, files: List[str]) -> Tuple[List[str], List[str]]:
618
+ """Replace any ``.socket.facts.json`` upload entry with a brotli-compressed ``.br`` sibling.
619
+
620
+ The Socket full-scan endpoint transparently decompresses a multipart part named
621
+ exactly ``.socket.facts.json.br``, so compressing here keeps a large facts file under
622
+ the server's per-file size cap without changing the stored result. Files whose
623
+ basename is not exactly ``.socket.facts.json`` are left untouched (the server only
624
+ matches that exact name), as are empty placeholder files (e.g. baseline scans).
625
+
626
+ Compression never blocks an upload: if it fails for any reason (missing optional
627
+ ``brotli`` dependency, unwritable directory, etc.) the original plain file is used.
628
+
629
+ Args:
630
+ files: The list of file paths about to be uploaded.
631
+
632
+ Returns:
633
+ ``(upload_files, temp_paths)`` where ``upload_files`` is the possibly-rewritten
634
+ list to upload and ``temp_paths`` are compressed files the caller must delete
635
+ once the upload completes.
636
+ """
637
+ upload_files: List[str] = []
638
+ temp_paths: List[str] = []
639
+ for file_path in files:
640
+ try:
641
+ if (
642
+ os.path.basename(file_path) == SOCKET_FACTS_FILENAME
643
+ and os.path.isfile(file_path)
644
+ and os.path.getsize(file_path) > 0
645
+ ):
646
+ compressed_path = self._compress_facts_file(file_path)
647
+ log.debug(
648
+ f"Brotli-compressed {file_path} for upload: "
649
+ f"{os.path.getsize(file_path)} -> {os.path.getsize(compressed_path)} bytes "
650
+ f"(uploading as {SOCKET_FACTS_BROTLI_FILENAME})"
651
+ )
652
+ upload_files.append(compressed_path)
653
+ temp_paths.append(compressed_path)
654
+ continue
655
+ except Exception as e:
656
+ # Never let compression break an upload: fall back to the plain file.
657
+ log.warning(
658
+ f"Failed to brotli-compress facts file {file_path}, uploading uncompressed: {e}"
659
+ )
660
+ upload_files.append(file_path)
661
+ return upload_files, temp_paths
662
+
547
663
  def create_full_scan(self, files: List[str], params: FullScanParams, base_paths: Optional[List[str]] = None) -> FullScan:
548
664
  """
549
665
  Creates a new full scan via the Socket API.
@@ -559,7 +675,19 @@ class Core:
559
675
  log.info("Creating new full scan")
560
676
  create_full_start = time.time()
561
677
 
562
- res = self.sdk.fullscans.post(files, params, use_types=True, use_lazy_loading=True, max_open_files=50, base_paths=base_paths)
678
+ # Brotli-compress the reachability facts file (if present) so it is uploaded as a
679
+ # `.socket.facts.json.br` part. The API decompresses it server-side, keeping a large
680
+ # facts file under the per-file upload size cap. See _compress_facts_files_for_upload.
681
+ upload_files, compressed_temp_files = self._compress_facts_files_for_upload(files)
682
+ try:
683
+ res = self.sdk.fullscans.post(upload_files, params, use_types=True, use_lazy_loading=True, max_open_files=50, base_paths=base_paths)
684
+ finally:
685
+ for temp_file in compressed_temp_files:
686
+ try:
687
+ os.unlink(temp_file)
688
+ log.debug(f"Cleaned up temporary compressed facts file: {temp_file}")
689
+ except OSError as cleanup_error:
690
+ log.debug(f"Failed to clean up temporary compressed facts file {temp_file}: {cleanup_error}")
563
691
  if not res.success:
564
692
  log.error(f"Error creating full scan: {res.message}, status: {res.status}")
565
693
  raise Exception(f"Error creating full scan: {res.message}, status: {res.status}")
@@ -0,0 +1,137 @@
1
+ """Tests for brotli compression of the reachability facts file on upload.
2
+
3
+ The Socket full-scan endpoint transparently decompresses a multipart part named exactly
4
+ `.socket.facts.json.br`, so the CLI compresses the facts file before uploading it. These
5
+ tests cover the helpers in `Core` that do that rewriting.
6
+ """
7
+ import json
8
+ import os
9
+
10
+ import pytest
11
+
12
+ try:
13
+ import brotli
14
+ except ImportError: # pragma: no cover - PyPy / non-CPython fallback
15
+ import brotlicffi as brotli
16
+
17
+ from socketsecurity.core import (
18
+ SOCKET_FACTS_BROTLI_FILENAME,
19
+ SOCKET_FACTS_FILENAME,
20
+ Core,
21
+ )
22
+
23
+
24
+ def _write(path, data: bytes):
25
+ with open(path, "wb") as f:
26
+ f.write(data)
27
+ return path
28
+
29
+
30
+ def test_compress_facts_file_roundtrips(tmp_path):
31
+ """The compressed sibling decompresses back to the exact original bytes."""
32
+ source = tmp_path / SOCKET_FACTS_FILENAME
33
+ payload = json.dumps({"components": [{"id": str(i)} for i in range(1000)]}).encode()
34
+ _write(str(source), payload)
35
+
36
+ compressed_path = Core._compress_facts_file(str(source))
37
+
38
+ # Compressed file is a sibling named exactly `.socket.facts.json.br`.
39
+ assert compressed_path == str(tmp_path / SOCKET_FACTS_BROTLI_FILENAME)
40
+ assert os.path.basename(compressed_path) == SOCKET_FACTS_BROTLI_FILENAME
41
+ # The original is untouched (other code paths still read it locally).
42
+ assert source.read_bytes() == payload
43
+ # Roundtrip matches.
44
+ with open(compressed_path, "rb") as f:
45
+ assert brotli.decompress(f.read()) == payload
46
+
47
+
48
+ def test_compress_for_upload_rewrites_facts_entry(tmp_path):
49
+ """A `.socket.facts.json` entry is replaced by its `.br` sibling; others pass through."""
50
+ core = Core.__new__(Core)
51
+ facts = _write(str(tmp_path / SOCKET_FACTS_FILENAME), b'{"a": 1}')
52
+ manifest = _write(str(tmp_path / "package.json"), b"{}")
53
+
54
+ upload_files, temp_paths = core._compress_facts_files_for_upload([facts, manifest])
55
+
56
+ expected_br = str(tmp_path / SOCKET_FACTS_BROTLI_FILENAME)
57
+ assert upload_files == [expected_br, manifest]
58
+ assert temp_paths == [expected_br]
59
+ assert os.path.isfile(expected_br)
60
+ # Non-facts files are never compressed.
61
+ assert manifest in upload_files
62
+
63
+
64
+ def test_compress_facts_file_removes_partial_output_on_failure(tmp_path, monkeypatch):
65
+ """If compression fails mid-stream, the half-written .br is removed (not orphaned)."""
66
+ source = _write(str(tmp_path / SOCKET_FACTS_FILENAME), b'{"a": 1}' * 1000)
67
+
68
+ class ExplodingCompressor:
69
+ def __init__(self, *args, **kwargs):
70
+ pass
71
+
72
+ def process(self, _data):
73
+ raise RuntimeError("disk full")
74
+
75
+ def finish(self): # pragma: no cover - never reached
76
+ return b""
77
+
78
+ # Patch the module the helper imports (brotli on CPython, brotlicffi elsewhere).
79
+ monkeypatch.setattr(brotli, "Compressor", ExplodingCompressor)
80
+
81
+ with pytest.raises(RuntimeError, match="disk full"):
82
+ Core._compress_facts_file(source)
83
+
84
+ # No orphaned .br left behind in the target directory.
85
+ assert not (tmp_path / SOCKET_FACTS_BROTLI_FILENAME).exists()
86
+
87
+
88
+ def test_compress_for_upload_preserves_directory_prefix(tmp_path):
89
+ """The `.br` sibling keeps the facts file's directory so the relative key is preserved."""
90
+ core = Core.__new__(Core)
91
+ subdir = tmp_path / "nested"
92
+ subdir.mkdir()
93
+ facts = _write(str(subdir / SOCKET_FACTS_FILENAME), b'{"a": 1}')
94
+
95
+ upload_files, temp_paths = core._compress_facts_files_for_upload([facts])
96
+
97
+ assert upload_files == [str(subdir / SOCKET_FACTS_BROTLI_FILENAME)]
98
+ assert temp_paths == [str(subdir / SOCKET_FACTS_BROTLI_FILENAME)]
99
+
100
+
101
+ def test_empty_facts_file_is_not_compressed(tmp_path):
102
+ """Empty placeholder facts files (e.g. baseline scans) are uploaded as-is."""
103
+ core = Core.__new__(Core)
104
+ empty_facts = _write(str(tmp_path / SOCKET_FACTS_FILENAME), b"")
105
+
106
+ upload_files, temp_paths = core._compress_facts_files_for_upload([empty_facts])
107
+
108
+ assert upload_files == [empty_facts]
109
+ assert temp_paths == []
110
+ assert not (tmp_path / SOCKET_FACTS_BROTLI_FILENAME).exists()
111
+
112
+
113
+ def test_custom_named_facts_file_is_not_compressed(tmp_path):
114
+ """A custom --reach-output-file name is not compressed (server only matches the exact name)."""
115
+ core = Core.__new__(Core)
116
+ custom = _write(str(tmp_path / "custom.facts.json"), b'{"a": 1}')
117
+
118
+ upload_files, temp_paths = core._compress_facts_files_for_upload([custom])
119
+
120
+ assert upload_files == [custom]
121
+ assert temp_paths == []
122
+
123
+
124
+ def test_compression_failure_falls_back_to_plain_file(tmp_path, monkeypatch):
125
+ """If compression raises, the original plain file is uploaded instead of failing."""
126
+ core = Core.__new__(Core)
127
+ facts = _write(str(tmp_path / SOCKET_FACTS_FILENAME), b'{"a": 1}')
128
+
129
+ def boom(_source_path):
130
+ raise RuntimeError("brotli unavailable")
131
+
132
+ monkeypatch.setattr(Core, "_compress_facts_file", staticmethod(boom))
133
+
134
+ upload_files, temp_paths = core._compress_facts_files_for_upload([facts])
135
+
136
+ assert upload_files == [facts]
137
+ assert temp_paths == []
@@ -1,6 +1,10 @@
1
1
  version = 1
2
2
  revision = 3
3
3
  requires-python = ">=3.11"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.13'",
6
+ "python_full_version < '3.13'",
7
+ ]
4
8
 
5
9
  [[package]]
6
10
  name = "anyio"
@@ -119,6 +123,79 @@ wheels = [
119
123
  { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
120
124
  ]
121
125
 
126
+ [[package]]
127
+ name = "brotli"
128
+ version = "1.2.0"
129
+ source = { registry = "https://pypi.org/simple" }
130
+ sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
131
+ wheels = [
132
+ { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" },
133
+ { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" },
134
+ { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" },
135
+ { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" },
136
+ { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" },
137
+ { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" },
138
+ { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" },
139
+ { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" },
140
+ { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" },
141
+ { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" },
142
+ { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" },
143
+ { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" },
144
+ { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" },
145
+ { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" },
146
+ { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" },
147
+ { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" },
148
+ { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" },
149
+ { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" },
150
+ { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" },
151
+ { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" },
152
+ { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
153
+ { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
154
+ { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
155
+ { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
156
+ { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
157
+ { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
158
+ { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
159
+ { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
160
+ { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
161
+ { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
162
+ { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" },
163
+ { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" },
164
+ { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" },
165
+ { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" },
166
+ { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" },
167
+ { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" },
168
+ { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" },
169
+ { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" },
170
+ { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" },
171
+ { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
172
+ ]
173
+
174
+ [[package]]
175
+ name = "brotlicffi"
176
+ version = "1.2.0.1"
177
+ source = { registry = "https://pypi.org/simple" }
178
+ dependencies = [
179
+ { name = "cffi" },
180
+ ]
181
+ sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" }
182
+ wheels = [
183
+ { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" },
184
+ { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" },
185
+ { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" },
186
+ { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" },
187
+ { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" },
188
+ { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" },
189
+ { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" },
190
+ { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" },
191
+ { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" },
192
+ { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" },
193
+ { url = "https://files.pythonhosted.org/packages/7f/53/6262c2256513e6f530d81642477cb19367270922063eaa2d7b781d8c723d/brotlicffi-1.2.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e015af99584c6db1490a69a210c765953e473e63adc2d891ac3062a737c9e851", size = 402265, upload-time = "2026-03-05T19:54:05.858Z" },
194
+ { url = "https://files.pythonhosted.org/packages/1f/d9/d5340b43cf5fbe7fe5a083d237e5338cc1caa73bea523be1c5e452c26290/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37cb587d32bf7168e2218c455e22e409ad1f3157c6c71945879a311f3e6b6abf", size = 406710, upload-time = "2026-03-05T19:54:07.272Z" },
195
+ { url = "https://files.pythonhosted.org/packages/a3/82/dbced4c1e0792efdf23fd90ff6d2a320c64ff4dfef7aacc85c04fde9ddd2/brotlicffi-1.2.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d6ba65dd528892b4d9960beba2ae011a753620bcfc66cf6fa3cee18d7b0baa4", size = 402787, upload-time = "2026-03-05T19:54:08.73Z" },
196
+ { url = "https://files.pythonhosted.org/packages/ef/6f/534205ba7590c9a8716a614f270c5c2ec419b5b7079b3f9cd31b7b5580de/brotlicffi-1.2.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2a5575653b0672638ba039b82fda56854934d7a6a24d4b8b5033f73ab43cbc1", size = 375108, upload-time = "2026-03-05T19:54:10.079Z" },
197
+ ]
198
+
122
199
  [[package]]
123
200
  name = "bs4"
124
201
  version = "0.0.2"
@@ -149,6 +226,8 @@ dependencies = [
149
226
  ]
150
227
  sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
151
228
  wheels = [
229
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
230
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
152
231
  { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
153
232
  { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
154
233
  { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
@@ -157,6 +236,11 @@ wheels = [
157
236
  { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
158
237
  { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
159
238
  { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
239
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
240
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
241
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
242
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
243
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
160
244
  { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
161
245
  { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
162
246
  { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
@@ -164,6 +248,11 @@ wheels = [
164
248
  { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
165
249
  { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
166
250
  { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
251
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
252
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
253
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
254
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
255
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
167
256
  { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
168
257
  { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
169
258
  { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
@@ -171,18 +260,31 @@ wheels = [
171
260
  { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
172
261
  { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
173
262
  { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
263
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
264
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
265
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
266
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
267
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
174
268
  { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
175
269
  { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
176
270
  { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
177
271
  { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
178
272
  { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
179
273
  { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
274
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
275
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
276
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
277
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
278
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
180
279
  { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
181
280
  { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
182
281
  { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
183
282
  { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
184
283
  { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
185
284
  { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
285
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
286
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
287
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
186
288
  ]
187
289
 
188
290
  [[package]]
@@ -612,7 +714,7 @@ name = "importlib-metadata"
612
714
  version = "8.7.1"
613
715
  source = { registry = "https://pypi.org/simple" }
614
716
  dependencies = [
615
- { name = "zipp" },
717
+ { name = "zipp", marker = "python_full_version < '3.13'" },
616
718
  ]
617
719
  sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
618
720
  wheels = [
@@ -1168,9 +1270,11 @@ wheels = [
1168
1270
 
1169
1271
  [[package]]
1170
1272
  name = "socketsecurity"
1171
- version = "2.3.0"
1273
+ version = "2.3.1"
1172
1274
  source = { editable = "." }
1173
1275
  dependencies = [
1276
+ { name = "brotli", marker = "platform_python_implementation == 'CPython'" },
1277
+ { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
1174
1278
  { name = "bs4" },
1175
1279
  { name = "gitpython" },
1176
1280
  { name = "markdown" },
@@ -1205,6 +1309,8 @@ dev = [
1205
1309
 
1206
1310
  [package.metadata]
1207
1311
  requires-dist = [
1312
+ { name = "brotli", marker = "platform_python_implementation == 'CPython'", specifier = ">=1.0.9" },
1313
+ { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'", specifier = ">=1.0.9" },
1208
1314
  { name = "bs4", specifier = ">=0.0.2" },
1209
1315
  { name = "gitpython" },
1210
1316
  { name = "hatch", marker = "extra == 'dev'" },
File without changes
File without changes
File without changes