energychain 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 (47) hide show
  1. energychain-0.1.0/LICENSE +21 -0
  2. energychain-0.1.0/MANIFEST.in +6 -0
  3. energychain-0.1.0/PKG-INFO +125 -0
  4. energychain-0.1.0/README.md +96 -0
  5. energychain-0.1.0/docs/architecture.md +33 -0
  6. energychain-0.1.0/docs/releasing.md +88 -0
  7. energychain-0.1.0/examples/custom_model.py +13 -0
  8. energychain-0.1.0/examples/quickstart.py +16 -0
  9. energychain-0.1.0/examples/solar_insights.py +13 -0
  10. energychain-0.1.0/pyproject.toml +36 -0
  11. energychain-0.1.0/scripts/check_dist.py +87 -0
  12. energychain-0.1.0/scripts/smoke_test.py +74 -0
  13. energychain-0.1.0/scripts/verify_pypi.py +72 -0
  14. energychain-0.1.0/setup.cfg +4 -0
  15. energychain-0.1.0/src/energychain/__init__.py +22 -0
  16. energychain-0.1.0/src/energychain/agents/__init__.py +4 -0
  17. energychain-0.1.0/src/energychain/agents/model.py +22 -0
  18. energychain-0.1.0/src/energychain/agents/runtime.py +45 -0
  19. energychain-0.1.0/src/energychain/client.py +36 -0
  20. energychain-0.1.0/src/energychain/connectors/__init__.py +6 -0
  21. energychain-0.1.0/src/energychain/connectors/base.py +23 -0
  22. energychain-0.1.0/src/energychain/connectors/csv.py +74 -0
  23. energychain-0.1.0/src/energychain/connectors/registry.py +54 -0
  24. energychain-0.1.0/src/energychain/connectors/simulator.py +71 -0
  25. energychain-0.1.0/src/energychain/exceptions.py +25 -0
  26. energychain-0.1.0/src/energychain/intelligence.py +68 -0
  27. energychain-0.1.0/src/energychain/memory/__init__.py +5 -0
  28. energychain-0.1.0/src/energychain/memory/base.py +38 -0
  29. energychain-0.1.0/src/energychain/memory/in_memory.py +52 -0
  30. energychain-0.1.0/src/energychain/memory/sqlite.py +91 -0
  31. energychain-0.1.0/src/energychain/models.py +149 -0
  32. energychain-0.1.0/src/energychain/observability.py +30 -0
  33. energychain-0.1.0/src/energychain/security.py +54 -0
  34. energychain-0.1.0/src/energychain/tools/__init__.py +4 -0
  35. energychain-0.1.0/src/energychain/tools/base.py +50 -0
  36. energychain-0.1.0/src/energychain/tools/builtin.py +77 -0
  37. energychain-0.1.0/src/energychain/workflows.py +37 -0
  38. energychain-0.1.0/src/energychain.egg-info/PKG-INFO +125 -0
  39. energychain-0.1.0/src/energychain.egg-info/SOURCES.txt +45 -0
  40. energychain-0.1.0/src/energychain.egg-info/dependency_links.txt +1 -0
  41. energychain-0.1.0/src/energychain.egg-info/requires.txt +6 -0
  42. energychain-0.1.0/src/energychain.egg-info/top_level.txt +1 -0
  43. energychain-0.1.0/tests/test_connector_and_tools.py +99 -0
  44. energychain-0.1.0/tests/test_csv_connector.py +38 -0
  45. energychain-0.1.0/tests/test_memory_and_workflows.py +43 -0
  46. energychain-0.1.0/tests/test_models.py +34 -0
  47. energychain-0.1.0/tests/test_release_checks.py +72 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EnergyChain Contributors
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,6 @@
1
+ include README.md
2
+ recursive-include docs *.md
3
+ recursive-include examples *.py
4
+ recursive-include scripts *.py
5
+ recursive-include tests *.py
6
+ prune .github
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: energychain
3
+ Version: 0.1.0
4
+ Summary: Composable, safety-first Python framework for AI-native energy applications
5
+ Author: EnergyChain Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Atanseiye/energychain
8
+ Project-URL: Repository, https://github.com/Atanseiye/energychain
9
+ Project-URL: Issues, https://github.com/Atanseiye/energychain/issues
10
+ Keywords: energy,solar,battery,AI,agents,energychain,SDK
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: build>=1; extra == "dev"
26
+ Requires-Dist: twine>=5; extra == "dev"
27
+ Requires-Dist: ruff>=0.6; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # EnergyChain
31
+
32
+ **EnergyChain is the developer infrastructure through which AI applications can understand, interact with, and safely optimize energy systems—from solar installations and batteries to mini-grids, utilities, and electricity networks.**
33
+
34
+ > Status: **v0.1.0 alpha**. Offline-first, Python-first, and read-only by default. Simulated energy data are **not live device measurements**.
35
+
36
+ EnergyChain brings composable, vendor-neutral energy data types, connectors, safe callable tools, storage, deterministic energy calculations, a pluggable agent interface, and in-process workflows to Python developers. EnergyChain is a **separate project** from Energy Data Network (EDN).
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ python -m pip install energychain
42
+ ```
43
+
44
+ To pin this release:
45
+
46
+ ```bash
47
+ python -m pip install energychain==0.1.0
48
+ ```
49
+
50
+ Requires Python 3.10 or newer. The core package uses **only Python standard-library runtime dependencies**.
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from energychain import EnergyChain
56
+
57
+ chain = EnergyChain.demo()
58
+ user = chain.principal("developer", allowed_sites={"demo-site"})
59
+
60
+ assets = chain.tools.invoke("discover_assets", user, "demo-site")
61
+ print([a.name for a in assets])
62
+
63
+ result = chain.tools.invoke(
64
+ "estimate_battery_runtime", user, "demo-site",
65
+ arguments={"battery_id": "demo-battery", "load_w": 900, "reserve_pct": 20},
66
+ )
67
+ print(result)
68
+ ```
69
+
70
+ **Security:** `principal()` is a **local-demo convenience**, not a production authentication mechanism. Production applications must authenticate principals, assign site-level permissions from a trusted service, and enforce authorization at API boundaries. The included connector simulator never sends live control commands.
71
+
72
+ ## Components available in 0.1
73
+
74
+ | Component | Implementation |
75
+ |---|---|
76
+ | Energy models | Site, asset, metric, reading, event, tariff, energy plan |
77
+ | Connectors | Abstract connector, registry, deterministic hybrid site simulator, CSV import |
78
+ | Memory | In-process bounded memory, SQLite durable persistence |
79
+ | Energy tools | Asset discovery, state, history, battery state, runtime and cost estimates, preferences |
80
+ | Safety | Permission scope, site boundaries and optional action-specific execution approval |
81
+ | Intelligence | Battery-runtime estimate, tariff cost, baseline mean forecast, outlier baseline |
82
+ | Agent | Explicit tool selection plus optional injected text-model callback |
83
+ | Workflow | In-process fail-closed sequential tool execution |
84
+ | Observability | In-memory bounded trace events |
85
+ | Developer experience | PyPI-ready packaging, examples, tests, CI and trusted-publisher release workflow |
86
+
87
+ ## Intentionally not included in 0.1
88
+
89
+ - Authenticated integrations with third-party solar/battery vendors (no verified API credentials or access yet)
90
+ - A production-ready control or dispatch runtime, remote device command implementation, or energy safety certification
91
+ - Autonomous LLM tool selection, multi-agent coordination, event subscriptions, distributed scheduling or durable workflow retries
92
+ - Production-grade multi-tenant identity, authorization server, encryption/key management, or high-volume telemetry infrastructure
93
+ - Trained forecasting models, validated anomaly detection accuracy, hardware testing or independently measured energy savings
94
+ - Managed EnergyChain Cloud, hosted APIs, TypeScript SDK, vector-search services or MCP adapter
95
+
96
+ These are subsequent product-development efforts, not features to advertise as shipped.
97
+
98
+ ## Connect existing CSV exports
99
+
100
+ Provide `asset_id,metric,value,timestamp,source` columns and register an explicit `EnergySite` and its `EnergyAsset` definitions. See `CSVConnector.from_csv(path, site=..., assets=...)`. The latest row is treated as the last *recorded* value (not necessarily live); caller must check freshness before operational decisions.
101
+
102
+ ## Design principles
103
+
104
+ 1. **Developer-first:** EnergyChain is software infrastructure for developers, not a national-grid control product.
105
+ 2. **Standard energy semantics:** W, Wh, %, UTC-offset-aware timestamps, data-source and quality fields.
106
+ 3. **Composable:** Connectors, tools, memory and language models can be independently replaced.
107
+ 4. **Data is not authority:** An LLM response cannot issue a physical command. Tools always check policies.
108
+ 5. **Provenance matters:** Simulated, estimated and measured values are explicitly labelled.
109
+ 6. **Tests and evidence:** Do not confuse synthetic demonstrations with field validation.
110
+
111
+ ## Local development
112
+
113
+ ```bash
114
+ python -m pip install -e '.[dev]'
115
+ python -m pytest -q
116
+ python -m build
117
+ python -m twine check dist/*
118
+ python examples/quickstart.py
119
+ ```
120
+
121
+ See [architecture](docs/architecture.md) for contract boundaries and limitations and [release guide](docs/releasing.md) for verified distribution steps.
122
+
123
+ ## Licensing and safety
124
+
125
+ EnergyChain is distributed under the [MIT License](LICENSE). Published wheels and source archives are publicly downloadable even though the development repository is private. Alpha-quality software: not warranted for physically controlling energy equipment.
@@ -0,0 +1,96 @@
1
+ # EnergyChain
2
+
3
+ **EnergyChain is the developer infrastructure through which AI applications can understand, interact with, and safely optimize energy systems—from solar installations and batteries to mini-grids, utilities, and electricity networks.**
4
+
5
+ > Status: **v0.1.0 alpha**. Offline-first, Python-first, and read-only by default. Simulated energy data are **not live device measurements**.
6
+
7
+ EnergyChain brings composable, vendor-neutral energy data types, connectors, safe callable tools, storage, deterministic energy calculations, a pluggable agent interface, and in-process workflows to Python developers. EnergyChain is a **separate project** from Energy Data Network (EDN).
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ python -m pip install energychain
13
+ ```
14
+
15
+ To pin this release:
16
+
17
+ ```bash
18
+ python -m pip install energychain==0.1.0
19
+ ```
20
+
21
+ Requires Python 3.10 or newer. The core package uses **only Python standard-library runtime dependencies**.
22
+
23
+ ## Quickstart
24
+
25
+ ```python
26
+ from energychain import EnergyChain
27
+
28
+ chain = EnergyChain.demo()
29
+ user = chain.principal("developer", allowed_sites={"demo-site"})
30
+
31
+ assets = chain.tools.invoke("discover_assets", user, "demo-site")
32
+ print([a.name for a in assets])
33
+
34
+ result = chain.tools.invoke(
35
+ "estimate_battery_runtime", user, "demo-site",
36
+ arguments={"battery_id": "demo-battery", "load_w": 900, "reserve_pct": 20},
37
+ )
38
+ print(result)
39
+ ```
40
+
41
+ **Security:** `principal()` is a **local-demo convenience**, not a production authentication mechanism. Production applications must authenticate principals, assign site-level permissions from a trusted service, and enforce authorization at API boundaries. The included connector simulator never sends live control commands.
42
+
43
+ ## Components available in 0.1
44
+
45
+ | Component | Implementation |
46
+ |---|---|
47
+ | Energy models | Site, asset, metric, reading, event, tariff, energy plan |
48
+ | Connectors | Abstract connector, registry, deterministic hybrid site simulator, CSV import |
49
+ | Memory | In-process bounded memory, SQLite durable persistence |
50
+ | Energy tools | Asset discovery, state, history, battery state, runtime and cost estimates, preferences |
51
+ | Safety | Permission scope, site boundaries and optional action-specific execution approval |
52
+ | Intelligence | Battery-runtime estimate, tariff cost, baseline mean forecast, outlier baseline |
53
+ | Agent | Explicit tool selection plus optional injected text-model callback |
54
+ | Workflow | In-process fail-closed sequential tool execution |
55
+ | Observability | In-memory bounded trace events |
56
+ | Developer experience | PyPI-ready packaging, examples, tests, CI and trusted-publisher release workflow |
57
+
58
+ ## Intentionally not included in 0.1
59
+
60
+ - Authenticated integrations with third-party solar/battery vendors (no verified API credentials or access yet)
61
+ - A production-ready control or dispatch runtime, remote device command implementation, or energy safety certification
62
+ - Autonomous LLM tool selection, multi-agent coordination, event subscriptions, distributed scheduling or durable workflow retries
63
+ - Production-grade multi-tenant identity, authorization server, encryption/key management, or high-volume telemetry infrastructure
64
+ - Trained forecasting models, validated anomaly detection accuracy, hardware testing or independently measured energy savings
65
+ - Managed EnergyChain Cloud, hosted APIs, TypeScript SDK, vector-search services or MCP adapter
66
+
67
+ These are subsequent product-development efforts, not features to advertise as shipped.
68
+
69
+ ## Connect existing CSV exports
70
+
71
+ Provide `asset_id,metric,value,timestamp,source` columns and register an explicit `EnergySite` and its `EnergyAsset` definitions. See `CSVConnector.from_csv(path, site=..., assets=...)`. The latest row is treated as the last *recorded* value (not necessarily live); caller must check freshness before operational decisions.
72
+
73
+ ## Design principles
74
+
75
+ 1. **Developer-first:** EnergyChain is software infrastructure for developers, not a national-grid control product.
76
+ 2. **Standard energy semantics:** W, Wh, %, UTC-offset-aware timestamps, data-source and quality fields.
77
+ 3. **Composable:** Connectors, tools, memory and language models can be independently replaced.
78
+ 4. **Data is not authority:** An LLM response cannot issue a physical command. Tools always check policies.
79
+ 5. **Provenance matters:** Simulated, estimated and measured values are explicitly labelled.
80
+ 6. **Tests and evidence:** Do not confuse synthetic demonstrations with field validation.
81
+
82
+ ## Local development
83
+
84
+ ```bash
85
+ python -m pip install -e '.[dev]'
86
+ python -m pytest -q
87
+ python -m build
88
+ python -m twine check dist/*
89
+ python examples/quickstart.py
90
+ ```
91
+
92
+ See [architecture](docs/architecture.md) for contract boundaries and limitations and [release guide](docs/releasing.md) for verified distribution steps.
93
+
94
+ ## Licensing and safety
95
+
96
+ EnergyChain is distributed under the [MIT License](LICENSE). Published wheels and source archives are publicly downloadable even though the development repository is private. Alpha-quality software: not warranted for physically controlling energy equipment.
@@ -0,0 +1,33 @@
1
+ # EnergyChain v0.1 architecture contract
2
+
3
+ ## Scope
4
+ Python framework for reading, normalizing, remembering, analyzing and explaining basic energy observations. Included connectors are a synthetic hybrid site simulator and a read-only CSV telemetry-import adapter; no branded live vendor API is claimed. No first-party hosted services or direct control integration is included.
5
+
6
+ ## Component dependency direction
7
+ `EnergyChain` assembles `ConnectorRegistry`, `EnergyMemory`, `ToolRegistry`, and `EnergyAgent`; workflow is an independent consumer of the tool registry. Energy adapters implement `EnergyConnector`. Authorized actions are checked at `ToolRegistry.invoke`. All default tool handlers are read/analysis only.
8
+
9
+ ## Domain conventions
10
+ - Power (instantaneous) = W; energy (integrated) = Wh. Do not interchange.
11
+ - Battery charge is percentage [0,100]; capacity is Wh.
12
+ - Datetime objects must contain timezone information.
13
+ - Source and data quality (measured, estimated, simulated) are mandatory for readings.
14
+ - Assets and sites use stable, provider-independent identifiers; registry disallows duplicate IDs across connectors.
15
+
16
+ ## Trust boundaries
17
+ - A caller constructs authenticated `Principal` from external authorization; `EnergyChain.principal()` is for demos only.
18
+ - Every tool invocation is site-scoped, and handlers validate an asset's registered site against that scope.
19
+ - A tool's permission level is not derived from LLM output.
20
+ - The model cannot dynamically invoke tools via `EnergyAgent` v0.1.
21
+ - Standard connector `execute` fails with `UnsupportedCapability` unless consciously implemented by a third-party adapter.
22
+ - Future physical execution must enforce equipment constraints, device authorization, human approval, auditable logs and post-action verification.
23
+ - Local SQLite and in-memory stores are not substitutes for production tenant isolation or infrastructure security.
24
+ - Tool return data must be treated as untrusted, and the runtime is not a prompt-injection defense for arbitrary LLM implementations.
25
+
26
+ ## Memory semantics
27
+ In-memory records are bounded per site; SQLite stores readings, events, notes and JSON-encoded preferences. The caller must ensure every recording matches the actual site identity. A dedicated production memory backend should enforce ownership at the storage layer and add retention/deletion policies.
28
+
29
+ ## Calculations
30
+ Battery runtime computes `capacity_wh * max(soc - reserve,0)/100 * efficiency / load_w`. A zero load returns `None` (unbounded in this idealized equation, not infinite guaranteed device runtime). Mean forecast and z-score anomaly detection are transparent baselines, not validated ML predictors. Financial calculation multiplies energy (Wh)/1000 by tariff currency per kWh.
31
+
32
+ ## Next compatibility points
33
+ Connectors: authenticated public vendor APIs; memory: production time-series store; models: external LLM APIs; workflows: checkpoints and idempotency; adapters: MCP/other agent runtimes; future optional hosted offering.
@@ -0,0 +1,88 @@
1
+ # Publishing the `energychain` distribution
2
+
3
+ **Do not claim `pip install energychain` works until the release exists on https://pypi.org/project/energychain/** and the installed package has been smoke-tested using a clean environment.
4
+
5
+ ## Owner decisions before the first public release
6
+
7
+ The repository is the existing **private** https://github.com/Atanseiye/energychain.
8
+ Continue its history; do not recreate it or change its visibility.
9
+
10
+ The owner selected the **MIT License** and approved public SDK distribution on
11
+ 26 October 2026. `LICENSE` contains the terms; `[project]` declares `license = "MIT"`
12
+ and `license-files = ["LICENSE"]`. The build requires `setuptools>=77` for PEP 639
13
+ license metadata. Keep the license in both wheels and source archives.
14
+
15
+ Public distribution includes the wheel **and source archive**. The publishing
16
+ workflow fails before upload if the built artifacts lack license metadata/files.
17
+ This is an artifact check, not proof of copyright ownership or legal clearance.
18
+
19
+ ## Account setup
20
+
21
+ 1. Check `https://pypi.org/pypi/energychain/json` immediately before publishing.
22
+ A 404 means no published project is visible; it does not guarantee the name is
23
+ reservable. If another party owns it, stop rather than uploading a substitute.
24
+ 2. Sign in at https://pypi.org/manage/account/publishing/ and add a pending GitHub
25
+ Trusted Publisher with these exact values:
26
+
27
+ | Field | Value |
28
+ | --- | --- |
29
+ | PyPI project | `energychain` |
30
+ | GitHub owner | `Atanseiye` |
31
+ | Repository | `energychain` |
32
+ | Workflow filename | `publish.yml` |
33
+ | Environment | `pypi` |
34
+
35
+ A pending publisher does not reserve the name. For an existing project owned by
36
+ this account, configure the publisher in that project's Publishing settings.
37
+ 3. In repository **Settings → Environments**, create `pypi`. Keep the repository
38
+ private. Set deployment rules/reviewers supported by the account as appropriate.
39
+ The workflow grants `id-token: write` only to the isolated publishing job;
40
+ no PyPI password or long-lived token belongs in the repository or its secrets.
41
+
42
+ These account steps require authenticated owner access. A committed workflow does
43
+ not configure the pending publisher in PyPI or verify the environment's settings.
44
+
45
+ ## Validate the release commit
46
+
47
+ ```bash
48
+ python -m pip install -e '.[dev]'
49
+ python -m ruff check --isolated --select E4,E7,E9,F src tests scripts examples
50
+ python -m pytest -q
51
+ python -m build
52
+ python -m twine check --strict dist/*
53
+ python scripts/check_dist.py --tag v0.1.0 --require-license
54
+ ```
55
+
56
+ Start with an empty `dist/` directory. `python -m build` builds the wheel from the
57
+ source archive, and `check_dist.py` checks that both contain the same SDK and
58
+ matching name/version metadata. The license check is mandatory for publishing.
59
+ Omit only `--require-license` when checking technical readiness before the owner
60
+ has selected terms; that does not authorize a public release.
61
+
62
+ Install the built wheel into a new virtualenv with `--no-index --no-deps`, then run
63
+ `python -I scripts/smoke_test.py` using that environment's Python. `-I` prevents the
64
+ checkout or `PYTHONPATH` from masking an incomplete wheel. CI does this on Python
65
+ 3.10–3.13 on Linux and Python 3.12 on Windows.
66
+
67
+ ## Publish and prove installation
68
+
69
+ 1. Ensure the exact release commit is on `main`, its CI is green, licensing is
70
+ settled and PyPI/account setup is complete.
71
+ 2. Create and publish a non-prerelease GitHub Release tagged `v0.1.0` at that commit.
72
+ `.github/workflows/publish.yml` repeats CI, verifies main ancestry and tag/version
73
+ agreement, validates licensing and tests the exact wheel before uploading.
74
+ 3. The separate `publish` job obtains short-lived PyPI credentials through GitHub
75
+ OIDC and publishes the previously tested artifacts. Actions are pinned to full
76
+ commit hashes. No code build or checkout runs in the OIDC-enabled job.
77
+ 4. The `verify` job runs `python scripts/verify_pypi.py`. It checks official PyPI
78
+ metadata and SHA256 hashes against the tested artifacts, creates a new Python
79
+ environment, and performs an **unpinned** install of `energychain` using only
80
+ `https://pypi.org/simple/`, without cache, local wheels or extra indexes. It checks
81
+ the installed version, official download URL, wheel hash, imports and SDK smoke
82
+ behavior. Success is reported only after that check passes.
83
+
84
+ If upload succeeds but verification fails, inspect the verification error before
85
+ retrying. PyPI release filenames cannot be overwritten. After a successful upload,
86
+ rerun only the failed verification job rather than uploading the same files again.
87
+
88
+ A **private GitHub repository does not make a PyPI distribution private**. Published wheels/sdists and their source can generally be downloaded by anyone. Decide whether the package source is suitable for public distribution before publishing.
@@ -0,0 +1,13 @@
1
+ """How to plug any proprietary or local LLM into EnergyChain."""
2
+ from energychain import CallableTextModel, EnergyChain
3
+
4
+
5
+ def my_llm(system: str, user: str, context: str) -> str:
6
+ # Replace with a provider SDK or local LLM call; a genuine LLM is not bundled.
7
+ return f"Prompt: {user}; available data: {context}"
8
+
9
+ chain = EnergyChain.demo()
10
+ p = chain.principal("developer", allowed_sites={"demo-site"})
11
+ agent = chain.agent(CallableTextModel(my_llm))
12
+ print(agent.run("What is my charge?", principal=p, site_id="demo-site",
13
+ tool_name="get_battery_state", tool_arguments={"asset_id": "demo-battery"}).answer)
@@ -0,0 +1,16 @@
1
+ """Run with: python examples/quickstart.py after pip install energychain"""
2
+ from energychain import EnergyChain
3
+
4
+ chain = EnergyChain.demo()
5
+ user = chain.principal("example-user", allowed_sites={"demo-site"})
6
+ print("Assets:", [asset.name for asset in chain.tools.invoke("discover_assets", user, "demo-site")])
7
+ state = chain.tools.invoke("get_battery_state", user, "demo-site",
8
+ arguments={"asset_id": "demo-battery"})
9
+ print("Battery charge:", state["state_of_charge"].value, "% (SIMULATED)")
10
+ estimate = chain.tools.invoke("estimate_battery_runtime", user, "demo-site",
11
+ arguments={"battery_id": "demo-battery", "load_w": 900})
12
+ print("Estimated backup hours:", round(estimate["hours"], 2))
13
+ response = chain.agent().run("Can the site supply backup power?", principal=user,
14
+ site_id="demo-site", tool_name="estimate_battery_runtime",
15
+ tool_arguments={"battery_id": "demo-battery", "load_w": 900})
16
+ print(response.answer)
@@ -0,0 +1,13 @@
1
+ """Synthetic 24-hour solar generation insights without a model or vendor credential."""
2
+ from datetime import datetime, timedelta, timezone
3
+ from energychain import EnergyChain, simple_average_forecast
4
+
5
+ chain = EnergyChain.demo()
6
+ principal = chain.principal("demo", allowed_sites={"demo-site"})
7
+ end = datetime.now(timezone.utc)
8
+ readings = chain.tools.invoke("get_energy_history", principal, "demo-site", arguments={
9
+ "asset_id": "demo-solar", "metric": "power_w", "start": end-timedelta(hours=23), "end": end})
10
+ forecast = simple_average_forecast(readings, horizon_hours=4)
11
+ print("Synthetic hourly readings:", len(readings))
12
+ print("Baseline forecast W:", forecast)
13
+ print("Warning: simulation and simple mean baseline, not a trained forecast or validated estimate.")
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "energychain"
7
+ version = "0.1.0"
8
+ license = "MIT"
9
+ license-files = ["LICENSE"]
10
+ description = "Composable, safety-first Python framework for AI-native energy applications"
11
+ readme = "README.md"
12
+ requires-python = ">=3.10"
13
+ authors = [{name = "EnergyChain Contributors"}]
14
+ keywords = ["energy", "solar", "battery", "AI", "agents", "energychain", "SDK"]
15
+ classifiers = ["Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries :: Python Modules"]
16
+
17
+ [project.optional-dependencies]
18
+ dev = ["pytest>=8", "build>=1", "twine>=5", "ruff>=0.6"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/Atanseiye/energychain"
22
+ Repository = "https://github.com/Atanseiye/energychain"
23
+ Issues = "https://github.com/Atanseiye/energychain/issues"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
30
+
31
+ [tool.ruff]
32
+ target-version = "py310"
33
+ line-length = 100
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E4", "E7", "E9", "F"]
@@ -0,0 +1,87 @@
1
+ """Check release artifacts without importing the working tree or contacting an index."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import ast
6
+ from email.parser import BytesParser
7
+ import hashlib
8
+ import json
9
+ from pathlib import Path
10
+ import tarfile
11
+ import zipfile
12
+
13
+
14
+ def check_distributions(dist: Path, *, tag: str | None = None,
15
+ require_license: bool = False) -> dict:
16
+ wheels = list(dist.glob("*.whl"))
17
+ sdists = list(dist.glob("*.tar.gz"))
18
+ if len(wheels) != 1 or len(sdists) != 1 or len(list(dist.iterdir())) != 2:
19
+ raise ValueError("dist must contain exactly one wheel and one source distribution")
20
+ with zipfile.ZipFile(wheels[0]) as wheel:
21
+ names = wheel.namelist()
22
+ metadata_name = next(n for n in names if n.endswith(".dist-info/METADATA"))
23
+ metadata = BytesParser().parsebytes(wheel.read(metadata_name))
24
+ init = ast.parse(wheel.read("energychain/__init__.py"))
25
+ source_version = next(
26
+ ast.literal_eval(n.value) for n in init.body if isinstance(n, ast.Assign)
27
+ and any(isinstance(t, ast.Name) and t.id == "__version__" for t in n.targets))
28
+ version = metadata["Version"]
29
+ if metadata["Name"] != "energychain" or version != source_version:
30
+ raise ValueError("distribution name/version does not match the SDK")
31
+ if wheels[0].name != f"energychain-{version}-py3-none-any.whl":
32
+ raise ValueError("unexpected wheel name or compatibility tag")
33
+ if metadata["Requires-Python"] != ">=3.10":
34
+ raise ValueError("unexpected Python compatibility metadata")
35
+ if tag is not None and tag != f"v{version}":
36
+ raise ValueError(f"release tag must be v{version}; received {tag!r}")
37
+ if any("extra ==" not in r for r in metadata.get_all("Requires-Dist", [])):
38
+ raise ValueError("core package must remain free of runtime dependencies")
39
+ license_terms = metadata["License-Expression"] or metadata["License"]
40
+ license_names = metadata.get_all("License-File", [])
41
+ if require_license:
42
+ if not license_terms or not license_names:
43
+ raise ValueError("owner-approved license metadata and LICENSE file are required")
44
+ license_payloads = [
45
+ wheel.read(n) for n in names
46
+ if any(n.endswith("/" + f) for f in license_names)]
47
+ if not license_payloads or not all(p.strip() for p in license_payloads):
48
+ raise ValueError("wheel is missing the selected licensing terms")
49
+ sdk_files = {n: wheel.read(n) for n in names if n.startswith("energychain/")}
50
+ with tarfile.open(sdists[0], "r:gz") as sdist:
51
+ prefix = f"energychain-{version}/"
52
+ pkg_info = sdist.extractfile(prefix + "PKG-INFO")
53
+ if pkg_info is None:
54
+ raise ValueError("source distribution has no PKG-INFO")
55
+ source_metadata = BytesParser().parsebytes(pkg_info.read())
56
+ if any(source_metadata[key] != metadata[key] for key in (
57
+ "Name", "Version", "Requires-Python", "License-Expression", "License")):
58
+ raise ValueError("wheel and source metadata differ")
59
+ source_names = {n.name for n in sdist.getmembers()}
60
+ if sdists[0].name != f"energychain-{version}.tar.gz":
61
+ raise ValueError("unexpected source distribution name")
62
+ for name, contents in sdk_files.items():
63
+ member = sdist.extractfile(prefix + "src/" + name)
64
+ if member is None or member.read() != contents:
65
+ raise ValueError(f"wheel/source SDK content differs: {name}")
66
+ if require_license and any(prefix + f not in source_names for f in license_names):
67
+ raise ValueError("source distribution is missing licensing terms")
68
+ return {
69
+ "name": "energychain", "version": version,
70
+ "license_selected": bool(license_terms and license_names),
71
+ "sha256": {p.name: hashlib.sha256(p.read_bytes()).hexdigest()
72
+ for p in sorted(wheels + sdists)},
73
+ }
74
+
75
+
76
+ if __name__ == "__main__":
77
+ parser = argparse.ArgumentParser(description=__doc__)
78
+ parser.add_argument("--dist", type=Path, default=Path("dist"))
79
+ parser.add_argument("--tag")
80
+ parser.add_argument("--require-license", action="store_true")
81
+ args = parser.parse_args()
82
+ try:
83
+ result = check_distributions(
84
+ args.dist, tag=args.tag, require_license=args.require_license)
85
+ except (ValueError, KeyError, StopIteration) as exc:
86
+ parser.exit(1, f"Release blocked: {exc}\n")
87
+ print(json.dumps(result, indent=2))
@@ -0,0 +1,74 @@
1
+ """Exercise an installed distribution. Run with python -I in a clean virtualenv."""
2
+ from datetime import datetime, timezone
3
+ from importlib.metadata import distribution
4
+ import json
5
+ from pathlib import Path
6
+ import tempfile
7
+
8
+ import energychain
9
+ from energychain import (
10
+ Approval, AssetKind, CSVConnector, EnergyAsset, EnergyChain, EnergySite,
11
+ Permission, Principal, SQLiteEnergyMemory, ToolRegistry, ToolSpec,
12
+ )
13
+ from energychain.exceptions import ApprovalRequired, PermissionDenied
14
+
15
+
16
+ def smoke_test():
17
+ installed = distribution("energychain")
18
+ assert installed.metadata["Name"] == "energychain"
19
+ assert installed.version == energychain.__version__
20
+ assert Path(energychain.__file__).resolve().is_relative_to(
21
+ Path(installed.locate_file("")).resolve()), "import resolved outside installed distribution"
22
+ assert not any("extra ==" not in r for r in (installed.requires or []))
23
+ for name in energychain.__all__:
24
+ assert hasattr(energychain, name), name
25
+ chain = EnergyChain.demo()
26
+ user = chain.principal("smoke", allowed_sites={"demo-site"})
27
+ assert len(chain.tools.invoke("discover_assets", user, "demo-site")) == 4
28
+ state = chain.tools.invoke("get_battery_state", user, "demo-site",
29
+ arguments={"asset_id": "demo-battery"})
30
+ assert state["state_of_charge"].quality == "simulated"
31
+ assert chain.tools.invoke("estimate_energy_cost", user, "demo-site",
32
+ arguments={"energy_wh": 2000, "price_per_kwh": 100})["cost"] == 200
33
+ try:
34
+ chain.tools.invoke("discover_assets", user, "other-site")
35
+ except PermissionDenied:
36
+ pass
37
+ else:
38
+ raise AssertionError("cross-site access was allowed")
39
+ tools = ToolRegistry()
40
+ tools.register(ToolSpec("act", "test-only command", Permission.EXECUTE,
41
+ lambda *, site_id: site_id))
42
+ operator = Principal("operator", frozenset({"site"}), frozenset({Permission.EXECUTE}))
43
+ try:
44
+ tools.invoke("act", operator, "site")
45
+ except ApprovalRequired:
46
+ pass
47
+ else:
48
+ raise AssertionError("execution did not require approval")
49
+ assert tools.invoke("act", operator, "site",
50
+ approval=Approval("operator", "site", "act", True)) == "site"
51
+ with tempfile.TemporaryDirectory() as directory:
52
+ csv = Path(directory) / "readings.csv"
53
+ csv.write_text("asset_id,metric,value,timestamp,source\n"
54
+ "load,power_w,100,2026-10-25T10:00:00Z,export\n", encoding="utf-8")
55
+ connector = CSVConnector.from_csv(
56
+ csv, site=EnergySite("site", "Site"),
57
+ assets=[EnergyAsset("load", "site", "Load", AssetKind.LOAD)])
58
+ reading = connector.read("load", "power_w")
59
+ assert reading.value == 100
60
+ database = Path(directory) / "readings.db"
61
+ with SQLiteEnergyMemory(database) as memory:
62
+ memory.add_reading("site", reading)
63
+ memory.set_preference("site", "reserve_pct", 20)
64
+ with SQLiteEnergyMemory(database) as memory:
65
+ assert memory.preferences("site") == {"reserve_pct": 20}
66
+ assert memory.history("site", "load", "power_w",
67
+ datetime(2026, 10, 25, tzinfo=timezone.utc),
68
+ datetime(2026, 10, 26, tzinfo=timezone.utc)) == [reading]
69
+ print(json.dumps({"name": installed.metadata["Name"], "version": installed.version,
70
+ "import_path": energychain.__file__, "smoke_test": "passed"}, indent=2))
71
+
72
+
73
+ if __name__ == "__main__":
74
+ smoke_test()