truesignal 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. truesignal-0.1.0/.gitignore +8 -0
  2. truesignal-0.1.0/LICENSE +21 -0
  3. truesignal-0.1.0/PKG-INFO +179 -0
  4. truesignal-0.1.0/README.md +144 -0
  5. truesignal-0.1.0/pyproject.toml +68 -0
  6. truesignal-0.1.0/src/truesignal/__init__.py +44 -0
  7. truesignal-0.1.0/src/truesignal/_util.py +28 -0
  8. truesignal-0.1.0/src/truesignal/cli.py +201 -0
  9. truesignal-0.1.0/src/truesignal/cli_helpers.py +314 -0
  10. truesignal-0.1.0/src/truesignal/connectors/__init__.py +48 -0
  11. truesignal-0.1.0/src/truesignal/connectors/cisa_kev.py +87 -0
  12. truesignal-0.1.0/src/truesignal/connectors/cloudflare_radar.py +96 -0
  13. truesignal-0.1.0/src/truesignal/connectors/gdelt.py +106 -0
  14. truesignal-0.1.0/src/truesignal/connectors/reddit.py +135 -0
  15. truesignal-0.1.0/src/truesignal/connectors/telegram.py +112 -0
  16. truesignal-0.1.0/src/truesignal/http.py +96 -0
  17. truesignal-0.1.0/src/truesignal/provenance/__init__.py +12 -0
  18. truesignal-0.1.0/src/truesignal/provenance/cache.py +97 -0
  19. truesignal-0.1.0/src/truesignal/provenance/stamp.py +98 -0
  20. truesignal-0.1.0/src/truesignal/py.typed +0 -0
  21. truesignal-0.1.0/src/truesignal/types.py +119 -0
  22. truesignal-0.1.0/tests/__init__.py +0 -0
  23. truesignal-0.1.0/tests/conftest.py +31 -0
  24. truesignal-0.1.0/tests/test_cache.py +75 -0
  25. truesignal-0.1.0/tests/test_cli.py +132 -0
  26. truesignal-0.1.0/tests/test_cli_helpers.py +253 -0
  27. truesignal-0.1.0/tests/test_connectors_cisa_kev.py +98 -0
  28. truesignal-0.1.0/tests/test_connectors_cloudflare_radar.py +91 -0
  29. truesignal-0.1.0/tests/test_connectors_gdelt.py +84 -0
  30. truesignal-0.1.0/tests/test_connectors_reddit.py +124 -0
  31. truesignal-0.1.0/tests/test_connectors_telegram.py +117 -0
  32. truesignal-0.1.0/tests/test_no_fabrication.py +275 -0
  33. truesignal-0.1.0/tests/test_stamp.py +105 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ .venv-freshinstall/
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ __pycache__/
7
+ *.pyc
8
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rudrendu Paul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: truesignal
3
+ Version: 0.1.0
4
+ Summary: Provenance-first OSINT/security intelligence feed CLI. Every item ships a real source URL, a real timestamp, and an explicit live/fallback flag -- never a fabricated or silently-replayed data point.
5
+ Project-URL: Homepage, https://github.com/RudrenduPaul/truesignal
6
+ Project-URL: Repository, https://github.com/RudrenduPaul/truesignal
7
+ Project-URL: Bug Tracker, https://github.com/RudrenduPaul/truesignal/issues
8
+ Project-URL: Changelog, https://github.com/RudrenduPaul/truesignal/blob/main/CHANGELOG.md
9
+ Project-URL: Documentation, https://github.com/RudrenduPaul/truesignal/blob/main/docs/getting-started.md
10
+ Project-URL: Author - Rudrendu Paul, https://github.com/RudrenduPaul
11
+ Project-URL: Author - Sourav Nandy, https://github.com/Sourav-nandy-ai
12
+ Author: Rudrendu Paul, Sourav Nandy
13
+ License-Expression: MIT
14
+ License-File: LICENSE
15
+ Keywords: cisa-kev,cli,cloudflare-radar,cve,gdelt,osint,provenance,security,threat-intelligence
16
+ Classifier: Development Status :: 3 - Alpha
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Topic :: Internet
28
+ Classifier: Topic :: Security
29
+ Requires-Python: >=3.9
30
+ Provides-Extra: dev
31
+ Requires-Dist: build<2,>=1.0; extra == 'dev'
32
+ Requires-Dist: pytest<9,>=7.0; extra == 'dev'
33
+ Requires-Dist: twine<7,>=5.0; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # truesignal (Python)
37
+
38
+ A provenance-first OSINT/security intelligence feed. Every item ships a real
39
+ source URL, a real upstream timestamp, and an explicit `live`/`fallback`
40
+ flag -- never a fabricated or silently-replayed data point.
41
+
42
+ [![PyPI version](https://img.shields.io/pypi/v/truesignal.svg)](https://pypi.org/project/truesignal/)
43
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/RudrenduPaul/truesignal/blob/main/LICENSE)
44
+ [![Python versions](https://img.shields.io/pypi/pyversions/truesignal.svg)](https://pypi.org/project/truesignal/)
45
+ [![CI](https://github.com/RudrenduPaul/truesignal/actions/workflows/ci.yml/badge.svg)](https://github.com/RudrenduPaul/truesignal/actions/workflows/ci.yml)
46
+
47
+ ## Why this exists
48
+
49
+ Personal OSINT/security feeds are easy to build and easy to get wrong in one
50
+ specific way: silently showing stale or synthetic data as if it were
51
+ current. TrueSignal's answer is structural, not a documentation promise --
52
+ every connector's failure path is enforced by a dedicated test suite to
53
+ return either a real cached item honestly labeled `fallback` with its real
54
+ age, or nothing at all. This package is the Python distribution -- a
55
+ genuine, independent port of the npm package, not a wrapper around the
56
+ Node binary.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ pip install truesignal
62
+ ```
63
+
64
+ or with [uv](https://docs.astral.sh/uv/):
65
+
66
+ ```bash
67
+ uv add truesignal
68
+ ```
69
+
70
+ No separate install step, no external binary to fetch. The complementary
71
+ JS/TS distribution installs the same way on the npm side:
72
+ `npm install -g truesignal-cli` (or `npx truesignal-cli init` to run it once
73
+ without installing) -- see the
74
+ [project README](https://github.com/RudrenduPaul/truesignal#readme) for
75
+ that package. Both are first-class, maintained together; neither is
76
+ deprecated in favor of the other.
77
+
78
+ ## Quickstart
79
+
80
+ ```bash
81
+ truesignal init
82
+ truesignal feed
83
+ ```
84
+
85
+ `init` reports which connectors are ready right now -- CISA-KEV and GDELT
86
+ need no configuration -- and which environment variables are still missing
87
+ for the rest (Cloudflare Radar, Reddit, Telegram). `feed` pulls from every
88
+ configured connector.
89
+
90
+ Or call the library directly (the agent-native path):
91
+
92
+ ```python
93
+ from truesignal import ALL_CONNECTORS
94
+
95
+ for connector in ALL_CONNECTORS:
96
+ if connector.requires_config and not connector.is_configured():
97
+ continue
98
+ for item in connector.fetch_items():
99
+ print(item.status, item.source, item.url)
100
+ ```
101
+
102
+ ## How it works
103
+
104
+ ```
105
+ connector.fetch_items()
106
+ -> fetch_with_fallback(source, fetch_live)
107
+ -> upstream API call succeeds -> stamp_live() -> write_cache() -> real "live" items
108
+ -> upstream API call fails -> read_cache() -> stamp_fallback() -> real "fallback"
109
+ items with an honest fallback_age_seconds, or []
110
+ if nothing has ever been cached
111
+ ```
112
+
113
+ Every connector implements the same `Connector` interface
114
+ (`truesignal/types.py`) and calls `fetch_with_fallback` instead of touching
115
+ the network directly -- that single function is what gives every connector
116
+ its live/fallback/nothing guarantee. See
117
+ [docs/concepts.md](https://github.com/RudrenduPaul/truesignal/blob/main/docs/concepts.md)
118
+ for the full data model and what each of the five connectors actually
119
+ returns.
120
+
121
+ ## No-fabrication guarantee
122
+
123
+ Every connector's live, fallback, and empty-cache-failure path is covered
124
+ by a dedicated pytest suite (`tests/test_no_fabrication.py`), ported
125
+ directly from the TypeScript suite's `no-fabrication.test.ts`: it proves,
126
+ for every connector, that a failed upstream fetch never produces
127
+ synthetic, randomized, or silently-relabeled-as-current data -- only a real
128
+ cached fallback item with an honest age, or nothing. A companion static
129
+ check scans every connector source file in `src/truesignal/connectors/`
130
+ for forbidden patterns (`random.random()`, a fake-data library,
131
+ `datetime.now()` used to construct an item's timestamp).
132
+
133
+ ## CLI command reference
134
+
135
+ ```
136
+ usage: truesignal [-h] [--version] {init,feed,verify} ...
137
+
138
+ Commands:
139
+ init [--json] Check which connectors are ready right now.
140
+ feed [--source NAME] [--json] Pull the current feed from every configured
141
+ connector, or one connector with --source.
142
+ verify <item-id> [--json] Re-fetch the source named in <item-id> and
143
+ confirm its current provenance.
144
+ ```
145
+
146
+ Exit codes: `0` success, `1` general error, `2` no connectors configured,
147
+ `3` network error (a configured connector's fetch failed with no fallback
148
+ data), `4` a malformed or unknown `verify` item id -- documented in full in
149
+ [docs/concepts.md](https://github.com/RudrenduPaul/truesignal/blob/main/docs/concepts.md).
150
+
151
+ ## Security
152
+
153
+ No connector ever `eval()`s, `exec()`s, or dynamically imports anything
154
+ read from an upstream response -- responses are only ever parsed as JSON
155
+ and mapped into typed fields. Credentials are read only from real
156
+ environment variables (`CLOUDFLARE_RADAR_API_TOKEN`, `REDDIT_CLIENT_ID`,
157
+ `REDDIT_CLIENT_SECRET`, `TELEGRAM_BOT_TOKEN`) -- never accepted as a CLI
158
+ flag, never auto-loaded from a `.env` file. To report a vulnerability, see
159
+ [SECURITY.md](https://github.com/RudrenduPaul/truesignal/blob/main/SECURITY.md).
160
+ **Honest note**: this project does not currently publish SLSA provenance,
161
+ Sigstore signatures, or an SBOM, and has no OpenSSF Scorecard badge set up
162
+ -- none of that infrastructure exists yet for either distribution, so it
163
+ isn't claimed here.
164
+
165
+ ## Contributing
166
+
167
+ See [CONTRIBUTING.md](https://github.com/RudrenduPaul/truesignal/blob/main/CONTRIBUTING.md)
168
+ for the full guide, covering both the TypeScript and Python codebases.
169
+
170
+ ```bash
171
+ cd python
172
+ python3 -m venv .venv && source .venv/bin/activate
173
+ pip install -e ".[dev]"
174
+ pytest
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT, see [LICENSE](https://github.com/RudrenduPaul/truesignal/blob/main/LICENSE).
@@ -0,0 +1,144 @@
1
+ # truesignal (Python)
2
+
3
+ A provenance-first OSINT/security intelligence feed. Every item ships a real
4
+ source URL, a real upstream timestamp, and an explicit `live`/`fallback`
5
+ flag -- never a fabricated or silently-replayed data point.
6
+
7
+ [![PyPI version](https://img.shields.io/pypi/v/truesignal.svg)](https://pypi.org/project/truesignal/)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/RudrenduPaul/truesignal/blob/main/LICENSE)
9
+ [![Python versions](https://img.shields.io/pypi/pyversions/truesignal.svg)](https://pypi.org/project/truesignal/)
10
+ [![CI](https://github.com/RudrenduPaul/truesignal/actions/workflows/ci.yml/badge.svg)](https://github.com/RudrenduPaul/truesignal/actions/workflows/ci.yml)
11
+
12
+ ## Why this exists
13
+
14
+ Personal OSINT/security feeds are easy to build and easy to get wrong in one
15
+ specific way: silently showing stale or synthetic data as if it were
16
+ current. TrueSignal's answer is structural, not a documentation promise --
17
+ every connector's failure path is enforced by a dedicated test suite to
18
+ return either a real cached item honestly labeled `fallback` with its real
19
+ age, or nothing at all. This package is the Python distribution -- a
20
+ genuine, independent port of the npm package, not a wrapper around the
21
+ Node binary.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install truesignal
27
+ ```
28
+
29
+ or with [uv](https://docs.astral.sh/uv/):
30
+
31
+ ```bash
32
+ uv add truesignal
33
+ ```
34
+
35
+ No separate install step, no external binary to fetch. The complementary
36
+ JS/TS distribution installs the same way on the npm side:
37
+ `npm install -g truesignal-cli` (or `npx truesignal-cli init` to run it once
38
+ without installing) -- see the
39
+ [project README](https://github.com/RudrenduPaul/truesignal#readme) for
40
+ that package. Both are first-class, maintained together; neither is
41
+ deprecated in favor of the other.
42
+
43
+ ## Quickstart
44
+
45
+ ```bash
46
+ truesignal init
47
+ truesignal feed
48
+ ```
49
+
50
+ `init` reports which connectors are ready right now -- CISA-KEV and GDELT
51
+ need no configuration -- and which environment variables are still missing
52
+ for the rest (Cloudflare Radar, Reddit, Telegram). `feed` pulls from every
53
+ configured connector.
54
+
55
+ Or call the library directly (the agent-native path):
56
+
57
+ ```python
58
+ from truesignal import ALL_CONNECTORS
59
+
60
+ for connector in ALL_CONNECTORS:
61
+ if connector.requires_config and not connector.is_configured():
62
+ continue
63
+ for item in connector.fetch_items():
64
+ print(item.status, item.source, item.url)
65
+ ```
66
+
67
+ ## How it works
68
+
69
+ ```
70
+ connector.fetch_items()
71
+ -> fetch_with_fallback(source, fetch_live)
72
+ -> upstream API call succeeds -> stamp_live() -> write_cache() -> real "live" items
73
+ -> upstream API call fails -> read_cache() -> stamp_fallback() -> real "fallback"
74
+ items with an honest fallback_age_seconds, or []
75
+ if nothing has ever been cached
76
+ ```
77
+
78
+ Every connector implements the same `Connector` interface
79
+ (`truesignal/types.py`) and calls `fetch_with_fallback` instead of touching
80
+ the network directly -- that single function is what gives every connector
81
+ its live/fallback/nothing guarantee. See
82
+ [docs/concepts.md](https://github.com/RudrenduPaul/truesignal/blob/main/docs/concepts.md)
83
+ for the full data model and what each of the five connectors actually
84
+ returns.
85
+
86
+ ## No-fabrication guarantee
87
+
88
+ Every connector's live, fallback, and empty-cache-failure path is covered
89
+ by a dedicated pytest suite (`tests/test_no_fabrication.py`), ported
90
+ directly from the TypeScript suite's `no-fabrication.test.ts`: it proves,
91
+ for every connector, that a failed upstream fetch never produces
92
+ synthetic, randomized, or silently-relabeled-as-current data -- only a real
93
+ cached fallback item with an honest age, or nothing. A companion static
94
+ check scans every connector source file in `src/truesignal/connectors/`
95
+ for forbidden patterns (`random.random()`, a fake-data library,
96
+ `datetime.now()` used to construct an item's timestamp).
97
+
98
+ ## CLI command reference
99
+
100
+ ```
101
+ usage: truesignal [-h] [--version] {init,feed,verify} ...
102
+
103
+ Commands:
104
+ init [--json] Check which connectors are ready right now.
105
+ feed [--source NAME] [--json] Pull the current feed from every configured
106
+ connector, or one connector with --source.
107
+ verify <item-id> [--json] Re-fetch the source named in <item-id> and
108
+ confirm its current provenance.
109
+ ```
110
+
111
+ Exit codes: `0` success, `1` general error, `2` no connectors configured,
112
+ `3` network error (a configured connector's fetch failed with no fallback
113
+ data), `4` a malformed or unknown `verify` item id -- documented in full in
114
+ [docs/concepts.md](https://github.com/RudrenduPaul/truesignal/blob/main/docs/concepts.md).
115
+
116
+ ## Security
117
+
118
+ No connector ever `eval()`s, `exec()`s, or dynamically imports anything
119
+ read from an upstream response -- responses are only ever parsed as JSON
120
+ and mapped into typed fields. Credentials are read only from real
121
+ environment variables (`CLOUDFLARE_RADAR_API_TOKEN`, `REDDIT_CLIENT_ID`,
122
+ `REDDIT_CLIENT_SECRET`, `TELEGRAM_BOT_TOKEN`) -- never accepted as a CLI
123
+ flag, never auto-loaded from a `.env` file. To report a vulnerability, see
124
+ [SECURITY.md](https://github.com/RudrenduPaul/truesignal/blob/main/SECURITY.md).
125
+ **Honest note**: this project does not currently publish SLSA provenance,
126
+ Sigstore signatures, or an SBOM, and has no OpenSSF Scorecard badge set up
127
+ -- none of that infrastructure exists yet for either distribution, so it
128
+ isn't claimed here.
129
+
130
+ ## Contributing
131
+
132
+ See [CONTRIBUTING.md](https://github.com/RudrenduPaul/truesignal/blob/main/CONTRIBUTING.md)
133
+ for the full guide, covering both the TypeScript and Python codebases.
134
+
135
+ ```bash
136
+ cd python
137
+ python3 -m venv .venv && source .venv/bin/activate
138
+ pip install -e ".[dev]"
139
+ pytest
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT, see [LICENSE](https://github.com/RudrenduPaul/truesignal/blob/main/LICENSE).
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "truesignal"
7
+ version = "0.1.0"
8
+ description = "Provenance-first OSINT/security intelligence feed CLI. Every item ships a real source URL, a real timestamp, and an explicit live/fallback flag -- never a fabricated or silently-replayed data point."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Rudrendu Paul" },
14
+ { name = "Sourav Nandy" },
15
+ ]
16
+ keywords = ["osint", "security", "threat-intelligence", "cisa-kev", "cve", "cli", "provenance", "cloudflare-radar", "gdelt"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Environment :: Console",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.9",
25
+ "Programming Language :: Python :: 3.10",
26
+ "Programming Language :: Python :: 3.11",
27
+ "Programming Language :: Python :: 3.12",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Topic :: Security",
30
+ "Topic :: Internet",
31
+ ]
32
+ dependencies = []
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=7.0,<9",
37
+ "build>=1.0,<2",
38
+ "twine>=5.0,<7",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/RudrenduPaul/truesignal"
43
+ Repository = "https://github.com/RudrenduPaul/truesignal"
44
+ "Bug Tracker" = "https://github.com/RudrenduPaul/truesignal/issues"
45
+ Changelog = "https://github.com/RudrenduPaul/truesignal/blob/main/CHANGELOG.md"
46
+ Documentation = "https://github.com/RudrenduPaul/truesignal/blob/main/docs/getting-started.md"
47
+ "Author - Rudrendu Paul" = "https://github.com/RudrenduPaul"
48
+ "Author - Sourav Nandy" = "https://github.com/Sourav-nandy-ai"
49
+
50
+ [project.scripts]
51
+ truesignal = "truesignal.cli:main"
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/truesignal"]
55
+
56
+ [tool.hatch.build.targets.sdist]
57
+ include = [
58
+ "src/truesignal",
59
+ "tests",
60
+ "README.md",
61
+ "LICENSE",
62
+ ]
63
+ exclude = [
64
+ "examples",
65
+ ]
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
@@ -0,0 +1,44 @@
1
+ """
2
+ Library entry point -- import truesignal's connectors and provenance layer programmatically.
3
+
4
+ from truesignal import ALL_CONNECTORS, get_connector
5
+
6
+ for connector in ALL_CONNECTORS:
7
+ for item in connector.fetch_items():
8
+ print(item.status, item.url)
9
+
10
+ This is the Python port of the truesignal-cli npm package
11
+ (https://www.npmjs.com/package/truesignal-cli). Both distributions ship the same five source
12
+ connectors (CISA-KEV, Cloudflare Radar, Reddit, Telegram, GDELT) and the same no-fabrication
13
+ provenance guarantee; see https://github.com/RudrenduPaul/truesignal for the canonical
14
+ documentation and the original TypeScript source.
15
+ """
16
+ from .connectors import ALL_CONNECTORS, get_connector
17
+ from .provenance import (
18
+ fetch_with_fallback,
19
+ get_cache_dir,
20
+ read_cache,
21
+ stamp_fallback,
22
+ stamp_live,
23
+ write_cache,
24
+ )
25
+ from .types import Connector, ConnectorNotConfiguredError, FeedItem, ItemStatus, UnstampedItem
26
+
27
+ __version__ = "0.1.0"
28
+
29
+ __all__ = [
30
+ "Connector",
31
+ "FeedItem",
32
+ "ItemStatus",
33
+ "UnstampedItem",
34
+ "ConnectorNotConfiguredError",
35
+ "ALL_CONNECTORS",
36
+ "get_connector",
37
+ "fetch_with_fallback",
38
+ "stamp_live",
39
+ "stamp_fallback",
40
+ "read_cache",
41
+ "write_cache",
42
+ "get_cache_dir",
43
+ "__version__",
44
+ ]
@@ -0,0 +1,28 @@
1
+ """Small internal helpers shared across connectors and the provenance layer. Not part of the
2
+ public API (hence the leading underscore) -- see truesignal/__init__.py for the public surface."""
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+
7
+
8
+ def format_iso8601(dt: datetime) -> str:
9
+ """
10
+ Formats a datetime as a JavaScript-``Date.prototype.toISOString()``-equivalent string:
11
+ always UTC, always exactly 3 millisecond digits, always a trailing "Z". Naive datetimes are
12
+ assumed to already be UTC (every caller in this codebase constructs them that way).
13
+ """
14
+ if dt.tzinfo is None:
15
+ dt = dt.replace(tzinfo=timezone.utc)
16
+ dt = dt.astimezone(timezone.utc)
17
+ milliseconds = dt.microsecond // 1000
18
+ return dt.strftime("%Y-%m-%dT%H:%M:%S") + f".{milliseconds:03d}Z"
19
+
20
+
21
+ def parse_iso8601(value: str) -> datetime:
22
+ """Parses a real upstream ISO-8601 timestamp (with or without a trailing 'Z') into an aware
23
+ UTC datetime. Raises ValueError on anything that isn't a real, parseable instant."""
24
+ normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
25
+ dt = datetime.fromisoformat(normalized)
26
+ if dt.tzinfo is None:
27
+ dt = dt.replace(tzinfo=timezone.utc)
28
+ return dt.astimezone(timezone.utc)