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.
- energychain-0.1.0/LICENSE +21 -0
- energychain-0.1.0/MANIFEST.in +6 -0
- energychain-0.1.0/PKG-INFO +125 -0
- energychain-0.1.0/README.md +96 -0
- energychain-0.1.0/docs/architecture.md +33 -0
- energychain-0.1.0/docs/releasing.md +88 -0
- energychain-0.1.0/examples/custom_model.py +13 -0
- energychain-0.1.0/examples/quickstart.py +16 -0
- energychain-0.1.0/examples/solar_insights.py +13 -0
- energychain-0.1.0/pyproject.toml +36 -0
- energychain-0.1.0/scripts/check_dist.py +87 -0
- energychain-0.1.0/scripts/smoke_test.py +74 -0
- energychain-0.1.0/scripts/verify_pypi.py +72 -0
- energychain-0.1.0/setup.cfg +4 -0
- energychain-0.1.0/src/energychain/__init__.py +22 -0
- energychain-0.1.0/src/energychain/agents/__init__.py +4 -0
- energychain-0.1.0/src/energychain/agents/model.py +22 -0
- energychain-0.1.0/src/energychain/agents/runtime.py +45 -0
- energychain-0.1.0/src/energychain/client.py +36 -0
- energychain-0.1.0/src/energychain/connectors/__init__.py +6 -0
- energychain-0.1.0/src/energychain/connectors/base.py +23 -0
- energychain-0.1.0/src/energychain/connectors/csv.py +74 -0
- energychain-0.1.0/src/energychain/connectors/registry.py +54 -0
- energychain-0.1.0/src/energychain/connectors/simulator.py +71 -0
- energychain-0.1.0/src/energychain/exceptions.py +25 -0
- energychain-0.1.0/src/energychain/intelligence.py +68 -0
- energychain-0.1.0/src/energychain/memory/__init__.py +5 -0
- energychain-0.1.0/src/energychain/memory/base.py +38 -0
- energychain-0.1.0/src/energychain/memory/in_memory.py +52 -0
- energychain-0.1.0/src/energychain/memory/sqlite.py +91 -0
- energychain-0.1.0/src/energychain/models.py +149 -0
- energychain-0.1.0/src/energychain/observability.py +30 -0
- energychain-0.1.0/src/energychain/security.py +54 -0
- energychain-0.1.0/src/energychain/tools/__init__.py +4 -0
- energychain-0.1.0/src/energychain/tools/base.py +50 -0
- energychain-0.1.0/src/energychain/tools/builtin.py +77 -0
- energychain-0.1.0/src/energychain/workflows.py +37 -0
- energychain-0.1.0/src/energychain.egg-info/PKG-INFO +125 -0
- energychain-0.1.0/src/energychain.egg-info/SOURCES.txt +45 -0
- energychain-0.1.0/src/energychain.egg-info/dependency_links.txt +1 -0
- energychain-0.1.0/src/energychain.egg-info/requires.txt +6 -0
- energychain-0.1.0/src/energychain.egg-info/top_level.txt +1 -0
- energychain-0.1.0/tests/test_connector_and_tools.py +99 -0
- energychain-0.1.0/tests/test_csv_connector.py +38 -0
- energychain-0.1.0/tests/test_memory_and_workflows.py +43 -0
- energychain-0.1.0/tests/test_models.py +34 -0
- 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,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()
|