fix: Adapt versioning scheme to Home Assistant and switch to uv (#896)
Some checks are pending
Bump Version / Bump Version Workflow (push) Waiting to run
docker-build / platform-excludes (push) Waiting to run
docker-build / build (push) Blocked by required conditions
docker-build / merge (push) Blocked by required conditions
pre-commit / pre-commit (push) Waiting to run
Run Pytest on Pull Request / test (push) Waiting to run

Home Assistant expects versioning always increases numbers. Add
a date component to the development version to comply with this
expectation. The scheme is now 0.0.0.dev<date><hash>.

Use uv for creating and managing the virtual environment for developement.
This enourmously speeds up dependency updates. For this change
dependency requirements are now solely handled in pyproject.toml.
requirements.tx and requirements-dev.txt are deleted.

Signed-off-by: Bobby Noelte <b0661n0e17e@gmail.com>
This commit is contained in:
Bobby Noelte
2026-02-23 20:59:03 +01:00
committed by GitHub
parent c578a56af2
commit d446274129
30 changed files with 3974 additions and 319 deletions

2
.env
View File

@@ -11,7 +11,7 @@ DOCKER_COMPOSE_DATA_DIR=${HOME}/.local/share/net.akkudoktor.eos
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Image / build # Image / build
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
VERSION=0.2.0.dev58204789 VERSION=config.yaml
PYTHON_VERSION=3.13.9 PYTHON_VERSION=3.13.9
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View File

@@ -18,15 +18,18 @@ jobs:
with: with:
python-version: "3.13.9" python-version: "3.13.9"
- name: Install dependencies - name: Install uv
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install -r requirements-dev.txt pip install uv
- name: Run Pytest - name: Sync environment with uv
run: | run: |
pip install -e . uv sync --extra dev
python -m pytest --finalize --check-config-side-effect -vs --cov src --cov-report term-missing
- name: Run tests
run: |
uv run pytest --finalize --check-config-side-effect -vs --cov src --cov-report term-missing
- name: Upload test artifacts - name: Upload test artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@@ -31,14 +31,14 @@ repos:
# --- Static type checking --- # --- Static type checking ---
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2 rev: v1.19.1
hooks: hooks:
- id: mypy - id: mypy
additional_dependencies: additional_dependencies:
- types-requests==2.32.4.20250913 - types-requests==2.32.4.20250913
- pandas-stubs==2.3.2.250926 - pandas-stubs==3.0.0.260204
- tokenize-rt==6.2.0 - tokenize-rt==6.2.0
- types-docutils==0.22.2.20251006 - types-docutils==0.22.3.20251115
- types-PyYaml==6.0.12.20250915 - types-PyYaml==6.0.12.20250915
pass_filenames: false pass_filenames: false
@@ -59,23 +59,29 @@ repos:
# Validate commit messages (using Python wrapper) # Validate commit messages (using Python wrapper)
- id: commitizen-commit - id: commitizen-commit
name: Commitizen (venv-aware) name: Commitizen (venv-aware)
entry: python3 scripts/cz_check_commit_message.py entry: scripts/cz_check_commit_message.py
language: system language: python
additional_dependencies:
- .
stages: [commit-msg] stages: [commit-msg]
pass_filenames: false pass_filenames: false
# Branch name check on push (using Python wrapper) # Branch name check on push (using Python wrapper)
- id: commitizen-branch - id: commitizen-branch
name: Commitizen branch check name: Commitizen branch check
entry: python3 scripts/cz_check_branch.py entry: scripts/cz_check_branch.py
language: system language: python
additional_dependencies:
- .
stages: [pre-push] stages: [pre-push]
pass_filenames: false pass_filenames: false
# Validate new commit messages before push (using Python wrapper) # Validate new commit messages before push (using Python wrapper)
- id: commitizen-new-commits - id: commitizen-new-commits
name: Commitizen (check new commits only, .venv aware) name: Commitizen (check new commits only, .venv aware)
entry: python3 -m scripts.cz_check_new_commits entry: scripts/cz_check_new_commits.py
language: system language: python
additional_dependencies:
- .
stages: [pre-push] stages: [pre-push]
pass_filenames: false pass_filenames: false

View File

@@ -16,7 +16,7 @@ in Home Assistant.
The prediction and measurement data can now be backed by a database. The database allows The prediction and measurement data can now be backed by a database. The database allows
to keep historic prediction data and measurement data for long time without keeping to keep historic prediction data and measurement data for long time without keeping
it in memory. The database supports backend selection, compression, incremental data load, it in memory. The database supports backend selection, compression, incremental data load,
automatic data saving to storage, automatic vaccum and compaction. Two database backends automatic data saving to storage, automatic vacuum and compaction. Two database backends
are integrated and can be configured, LMDB and SQLight3. are integrated and can be configured, LMDB and SQLight3.
In addition, bugs were fixed and new features were added. In addition, bugs were fixed and new features were added.
@@ -63,7 +63,7 @@ In addition, bugs were fixed and new features were added.
real (test) environment pathes. real (test) environment pathes.
- development version scheme - development version scheme
The development versioning scheme is adaptet to fit to docker and The development versioning scheme is adaptet to fit to docker and
home assistant expectations. The new scheme is x.y.z and x.y.z.dev<hash>. home assistant expectations. The new scheme is x.y.z and x.y.z.dev'date''hash'.
Hash is only digits as expected by home assistant. Development version Hash is only digits as expected by home assistant. Development version
is appended by .dev as expected by docker. is appended by .dev as expected by docker.
- use mean value in interval on resampling for array - use mean value in interval on resampling for array
@@ -133,6 +133,8 @@ In addition, bugs were fixed and new features were added.
- add home assistant add-on development environment - add home assistant add-on development environment
Add VSCode devcontainer and task definition for home assistant add-on Add VSCode devcontainer and task definition for home assistant add-on
development. development.
- Use uv to manage the virtual environment for development.
This enormously increases dependency updates.
- improve documentation - improve documentation
## 0.2.0 (2025-11-09) ## 0.2.0 (2025-11-09)

View File

@@ -59,20 +59,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libopenblas-dev liblapack-dev \ libopenblas-dev liblapack-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# - Copy project metadata first (better Docker layer caching)
COPY pyproject.toml .
# - Create venv # - Create venv
RUN python3 -m venv ${VENV_PATH} RUN python3 -m venv ${VENV_PATH}
# - Upgrade pip inside venv # - Upgrade pip inside venv
RUN pip install --upgrade pip setuptools wheel RUN pip install --upgrade pip setuptools wheel
# - Install deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install EOS/ EOSdash # Install EOS/ EOSdash
# - Copy source # - Copy source
COPY src/ ./src COPY src/ ./src
COPY pyproject.toml .
# - Create version information # - Create version information
COPY scripts/get_version.py ./scripts/get_version.py COPY scripts/get_version.py ./scripts/get_version.py

123
Makefile
View File

@@ -1,8 +1,18 @@
# Define the targets # Define the targets
.PHONY: help venv pip install dist test test-full test-system test-ci test-profile docker-run docker-build docs read-docs clean format gitlint mypy run run-dev run-dash run-dash-dev prepare-version test-version .PHONY: help install dist test test-system test-ci test-profile \
docker-run docker-build docs read-docs clean format gitlint mypy \
run run-dev run-dash run-dash-dev prepare-version test-version uv-update
# Use uv for all program actions
UV := uv
PYTHON := $(UV) run python
PYTEST := $(UV) run pytest
MYPY := $(UV) run mypy
PRECOMMIT := $(UV) run pre-commit
COMMITIZEN := $(UV) run cz
# - Take VERSION from version.py # - Take VERSION from version.py
VERSION := $(shell python3 scripts/get_version.py) VERSION := $(shell $(PYTHON) scripts/get_version.py)
# Default target # Default target
all: help all: help
@@ -10,13 +20,11 @@ all: help
# Target to display help information # Target to display help information
help: help:
@echo "Available targets:" @echo "Available targets:"
@echo " venv - Set up a Python 3 virtual environment."
@echo " pip - Install dependencies from requirements.txt."
@echo " pip-dev - Install dependencies from requirements-dev.txt."
@echo " format - Format source code." @echo " format - Format source code."
@echo " gitlint - Lint last commit message." @echo " gitlint - Lint last commit message."
@echo " mypy - Run mypy." @echo " mypy - Run mypy."
@echo " install - Install EOS in editable form (development mode) into virtual environment." @echo " install - Install EOS in editable form (development mode) into virtual environment."
@echo " update-env - Update virtual environmenr to match pyproject.toml."
@echo " docker-run - Run entire setup on docker" @echo " docker-run - Run entire setup on docker"
@echo " docker-build - Rebuild docker image" @echo " docker-build - Rebuild docker image"
@echo " docs - Generate HTML documentation (in build/docs/html/)." @echo " docs - Generate HTML documentation (in build/docs/html/)."
@@ -36,57 +44,48 @@ help:
@echo " clean - Remove generated documentation, distribution and virtual environment." @echo " clean - Remove generated documentation, distribution and virtual environment."
@echo " prepare-version - Prepare a version defined in setup.py." @echo " prepare-version - Prepare a version defined in setup.py."
# Target to set up a Python 3 virtual environment
venv:
python3 -m venv .venv
@PYVER=$$(./.venv/bin/python --version) && \
echo "Virtual environment created in '.venv' with $$PYVER. Activate it using 'source .venv/bin/activate'."
# Target to install dependencies from requirements.txt
pip: venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r requirements.txt
@echo "Dependencies installed from requirements.txt."
# Target to install dependencies from requirements.txt
pip-dev: pip
.venv/bin/pip install -r requirements-dev.txt
@echo "Dependencies installed from requirements-dev.txt."
# Target to create a version.txt # Target to create a version.txt
version-txt: version-txt:
echo "$(VERSION)" > version.txt # Get the version from the package for setuptools (and pip)
VERSION=$$(${PYTHON} scripts/get_version.py)
@echo "$(VERSION)" > version.txt
@echo "version.txt set to '$(VERSION)'."
# Target to install EOS in editable form (development mode) into virtual environment. # Target to install EOS in editable form (development mode) into virtual environment.
install: pip-dev version-txt install: version-txt
.venv/bin/pip install build # Upgrade installation and dependencies
.venv/bin/pip install -e . $(UV) sync --extra dev
@echo "EOS installed in editable form (development mode)." @echo "EOS version $(VERSION) installed in editable form (development mode)."
# Target to rebuild the virtual environment.
update-env:
@echo "Rebuilding virtual environment to match pyproject.toml..."
uv rebuild
@echo "Environment rebuilt."
# Target to create a distribution. # Target to create a distribution.
dist: pip dist: version-txt
.venv/bin/pip install build $(PIP) install build
.venv/bin/python -m build --wheel $(PYTHON) -m build --wheel
@echo "Distribution created (see dist/)." @echo "Distribution created (see dist/)."
# Target to generate documentation # Target to generate documentation
gen-docs: pip-dev version-txt gen-docs: version-txt
.venv/bin/pip install -e . $(PYTHON) ./scripts/generate_config_md.py --output-file docs/_generated/config.md
.venv/bin/python ./scripts/generate_config_md.py --output-file docs/_generated/config.md $(PYTHON) ./scripts/generate_openapi_md.py --output-file docs/_generated/openapi.md
.venv/bin/python ./scripts/generate_openapi_md.py --output-file docs/_generated/openapi.md $(PYTHON) ./scripts/generate_openapi.py --output-file openapi.json
.venv/bin/python ./scripts/generate_openapi.py --output-file openapi.json
@echo "Documentation generated to openapi.json and docs/_generated." @echo "Documentation generated to openapi.json and docs/_generated."
# Target to build HTML documentation # Target to build HTML documentation
docs: pip-dev docs: install
.venv/bin/pytest --finalize tests/test_docsphinx.py $(PYTEST) --finalize tests/test_docsphinx.py
@echo "Documentation build to build/docs/html/." @echo "Documentation build to build/docs/html/."
# Target to read the HTML documentation # Target to read the HTML documentation
read-docs: read-docs:
@echo "Read the documentation in your browser" @echo "Read the documentation in your browser"
.venv/bin/pytest --finalize tests/test_docsphinx.py $(PYTEST) --finalize tests/test_docsphinx.py
.venv/bin/python -m webbrowser build/docs/html/index.html $(PYTHON) -m webbrowser build/docs/html/index.html
# Clean Python bytecode # Clean Python bytecode
clean-bytecode: clean-bytecode:
@@ -104,64 +103,66 @@ clean-docs:
clean: clean-docs clean: clean-docs
@echo "Cleaning virtual env, distribution and build directories" @echo "Cleaning virtual env, distribution and build directories"
rm -rf build .venv rm -rf build .venv
@echo "Cleaning uv environment"
$(UV) clean
@echo "Deletion complete." @echo "Deletion complete."
run: run:
@echo "Starting EOS production server, please wait..." @echo "Starting EOS production server, please wait..."
.venv/bin/python -m akkudoktoreos.server.eos --startup_eosdash true $(PYTHON) -m akkudoktoreos.server.eos --startup_eosdash true
run-dev: run-dev:
@echo "Starting EOS development server, please wait..." @echo "Starting EOS development server, please wait..."
.venv/bin/python -m akkudoktoreos.server.eos --host localhost --port 8503 --log_level DEBUG --startup_eosdash false --reload true $(PYTHON) -m akkudoktoreos.server.eos --host localhost --port 8503 --log_level DEBUG --startup_eosdash false --reload true
run-dash: run-dash:
@echo "Starting EOSdash production server, please wait..." @echo "Starting EOSdash production server, please wait..."
.venv/bin/python -m akkudoktoreos.server.eosdash $(PYTHON) -m akkudoktoreos.server.eosdash
run-dash-dev: run-dash-dev:
@echo "Starting EOSdash development server, please wait..." @echo "Starting EOSdash development server, please wait..."
.venv/bin/python -m akkudoktoreos.server.eosdash --host localhost --port 8504 --log_level DEBUG --reload true $(PYTHON) -m akkudoktoreos.server.eosdash --host localhost --port 8504 --log_level DEBUG --reload true
# Target to setup tests. # Target to setup tests.
test-setup: pip-dev test-setup: install
@echo "Setup tests" @echo "Setup tests"
# Target to run tests. # Target to run tests.
test: test:
@echo "Running tests..." @echo "Running tests..."
.venv/bin/pytest -vs --cov src --cov-report term-missing $(PYTEST) -vs --cov src --cov-report term-missing
# Target to run tests as done by CI on Github. # Target to run tests as done by CI on Github.
test-ci: test-ci:
@echo "Running tests as CI..." @echo "Running tests as CI..."
.venv/bin/pytest --finalize --check-config-side-effect -vs --cov src --cov-report term-missing $(PYTEST) --finalize --check-config-side-effect -vs --cov src --cov-report term-missing
# Target to run tests including the system tests. # Target to run tests including the system tests.
test-system: test-system:
@echo "Running tests incl. system tests..." @echo "Running tests incl. system tests..."
.venv/bin/pytest --system-test -vs --cov src --cov-report term-missing $(PYTEST) --system-test -vs --cov src --cov-report term-missing
# Target to run all tests. # Target to run all tests.
test-finalize: test-finalize:
@echo "Running all tests..." @echo "Running all tests..."
.venv/bin/pytest --finalize $(PYTEST) --finalize
# Target to run tests including the single test optimization with profiling. # Target to run tests including the single test optimization with profiling.
test-profile: test-profile:
@echo "Running single test optimization with profiling..." @echo "Running single test optimization with profiling..."
.venv/bin/python tests/single_test_optimization.py --profile $(PYTHON) tests/single_test_optimization.py --profile
# Target to format code. # Target to format code.
format: format:
.venv/bin/pre-commit run --all-files $(PRECOMMIT) run --all-files
# Target to trigger gitlint using pre-commit for the latest commit messages # Target to trigger git linting using commitizen for the latest commit messages
gitlint: gitlint:
.venv/bin/cz check --rev-range main..HEAD $(COMMITIZEN) check --rev-range main..HEAD
# Target to format code. # Target to format code.
mypy: mypy:
.venv/bin/mypy $(MYPY)
# Run entire setup on docker # Run entire setup on docker
docker-run: docker-run:
@@ -180,15 +181,15 @@ docker-build:
# Take UPDATE_FILES from GitHub action bump-version.yml # Take UPDATE_FILES from GitHub action bump-version.yml
UPDATE_FILES := $(shell sed -n 's/^[[:space:]]*UPDATE_FILES[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \ UPDATE_FILES := $(shell sed -n 's/^[[:space:]]*UPDATE_FILES[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
.github/workflows/bump-version.yml) .github/workflows/bump-version.yml)
prepare-version: #pip-dev prepare-version: install
@echo "Update version to $(VERSION) from version.py in files $(UPDATE_FILES) and doc" @echo "Update version to $(VERSION) from version.py in files $(UPDATE_FILES) and doc"
.venv/bin/python ./scripts/update_version.py $(VERSION) $(UPDATE_FILES) $(PYTHON) ./scripts/update_version.py $(VERSION) $(UPDATE_FILES)
.venv/bin/python ./scripts/convert_lightweight_tags.py $(PYTHON) ./scripts/convert_lightweight_tags.py
.venv/bin/python ./scripts/generate_config_md.py --output-file docs/_generated/config.md $(PYTHON) ./scripts/generate_config_md.py --output-file docs/_generated/config.md
.venv/bin/python ./scripts/generate_openapi_md.py --output-file docs/_generated/openapi.md $(PYTHON) ./scripts/generate_openapi_md.py --output-file docs/_generated/openapi.md
.venv/bin/python ./scripts/generate_openapi.py --output-file openapi.json $(PYTHON) ./scripts/generate_openapi.py --output-file openapi.json
.venv/bin/pytest -vv --finalize tests/test_version.py $(PYTEST) -vv --finalize tests/test_version.py
test-version: test-version:
echo "Test version information to be correctly set in all version files" echo "Test version information to be correctly set in all version files"
.venv/bin/pytest -vv tests/test_version.py $(PYTEST) -vv tests/test_version.py

View File

@@ -6,7 +6,7 @@
# the root directory (no add-on folder as usual). # the root directory (no add-on folder as usual).
name: "Akkudoktor-EOS" name: "Akkudoktor-EOS"
version: "0.2.0.dev58204789" version: "0.2.0.dev2602231150315077"
slug: "eos" slug: "eos"
description: "Akkudoktor-EOS add-on" description: "Akkudoktor-EOS add-on"
url: "https://github.com/Akkudoktor-EOS/EOS" url: "https://github.com/Akkudoktor-EOS/EOS"

View File

@@ -120,7 +120,7 @@
} }
}, },
"general": { "general": {
"version": "0.2.0.dev58204789", "version": "0.2.0.dev2602231150315077",
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net", "data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
"data_output_subpath": "output", "data_output_subpath": "output",
"latitude": 52.52, "latitude": 52.52,

View File

@@ -16,7 +16,7 @@
| latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees between -90 and 90. North is positive (ISO 19115) (°) | | latitude | `EOS_GENERAL__LATITUDE` | `Optional[float]` | `rw` | `52.52` | Latitude in decimal degrees between -90 and 90. North is positive (ISO 19115) (°) |
| longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees within -180 to 180 (°) | | longitude | `EOS_GENERAL__LONGITUDE` | `Optional[float]` | `rw` | `13.405` | Longitude in decimal degrees within -180 to 180 (°) |
| timezone | | `Optional[str]` | `ro` | `N/A` | Computed timezone based on latitude and longitude. | | timezone | | `Optional[str]` | `ro` | `N/A` | Computed timezone based on latitude and longitude. |
| version | `EOS_GENERAL__VERSION` | `str` | `rw` | `0.2.0.dev58204789` | Configuration file version. Used to check compatibility. | | version | `EOS_GENERAL__VERSION` | `str` | `rw` | `0.2.0.dev2602231150315077` | Configuration file version. Used to check compatibility. |
::: :::
<!-- pyml enable line-length --> <!-- pyml enable line-length -->
@@ -28,7 +28,7 @@
```json ```json
{ {
"general": { "general": {
"version": "0.2.0.dev58204789", "version": "0.2.0.dev2602231150315077",
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net", "data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
"data_output_subpath": "output", "data_output_subpath": "output",
"latitude": 52.52, "latitude": 52.52,
@@ -46,7 +46,7 @@
```json ```json
{ {
"general": { "general": {
"version": "0.2.0.dev58204789", "version": "0.2.0.dev2602231150315077",
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net", "data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
"data_output_subpath": "output", "data_output_subpath": "output",
"latitude": 52.52, "latitude": 52.52,

View File

@@ -1,6 +1,6 @@
# Akkudoktor-EOS # Akkudoktor-EOS
**Version**: `v0.2.0.dev58204789` **Version**: `v0.2.0.dev2602231150315077`
<!-- pyml disable line-length --> <!-- pyml disable line-length -->
**Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period. **Description**: This project provides a comprehensive solution for simulating and optimizing an energy system based on renewable energy sources. With a focus on photovoltaic (PV) systems, battery storage (batteries), load management (consumer requirements), heat pumps, electric vehicles, and consideration of electricity price data, this system enables forecasting and optimization of energy flow and costs over a specified period.

View File

@@ -27,13 +27,13 @@ Example:
.. code-block:: powershell .. code-block:: powershell
.venv\Scripts\python src/akkudoktoreos/server/eos.py --log-level DEBUG uv run python src/akkudoktoreos/server/eos.py --log-level DEBUG
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
.venv/bin/python src/akkudoktoreos/server/eos.py --log-level DEBUG uv run python src/akkudoktoreos/server/eos.py --log-level DEBUG
``` ```

View File

@@ -80,21 +80,15 @@ This is recommended for developers who want to modify the source code and test c
.. code-block:: powershell .. code-block:: powershell
python -m venv .venv uv run python scripts/get_version.py > version.txt
.venv\Scripts\pip install --upgrade pip uv sync --extra dev
.venv\Scripts\pip install -r requirements-dev.txt
.venv\Scripts\pip install build
.venv\Scripts\pip install -e .
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
python3 -m venv .venv uv run python scripts/get_version.py > version.txt
.venv/bin/pip install --upgrade pip uv sync --extra dev
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/pip install build
.venv/bin/pip install -e .
.. tab:: Linux Make .. tab:: Linux Make
@@ -103,25 +97,7 @@ This is recommended for developers who want to modify the source code and test c
make install make install
``` ```
### Step 2.2 Activate the Virtual Environment ### Step 2.2 - Install pre-commit
```{eval-rst}
.. tabs::
.. tab:: Windows
.. code-block:: powershell
.venv\Scripts\activate.bat
.. tab:: Linux
.. code-block:: bash
source .venv/bin/activate
```
### Step 2.3 - Install pre-commit
Our code style and commit message checks use [`pre-commit`](https://pre-commit.com). Our code style and commit message checks use [`pre-commit`](https://pre-commit.com).
@@ -157,13 +133,13 @@ Make EOS accessible at [http://localhost:8503/docs](http://localhost:8503/docs)
.. code-block:: powershell .. code-block:: powershell
python -m akkudoktoreos.server.eos uv run python -m akkudoktoreos.server.eos
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
python -m akkudoktoreos.server.eos uv run python -m akkudoktoreos.server.eos
.. tab:: Linux Make .. tab:: Linux Make
@@ -194,13 +170,13 @@ interfere with the EOS server trying to start EOSdash.
.. code-block:: powershell .. code-block:: powershell
python -m akkudoktoreos.server.eosdash --host localhost --port 8504 --log_level DEBUG --reload true uv run python -m akkudoktoreos.server.eosdash --host localhost --port 8504 --log_level DEBUG --reload true
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
python -m akkudoktoreos.server.eosdash --host localhost --port 8504 --log_level DEBUG --reload true uv run python -m akkudoktoreos.server.eosdash --host localhost --port 8504 --log_level DEBUG --reload true
.. tab:: Linux Make .. tab:: Linux Make
@@ -216,13 +192,13 @@ interfere with the EOS server trying to start EOSdash.
.. code-block:: powershell .. code-block:: powershell
python -m akkudoktoreos.server.eos --host localhost --port 8503 --log_level DEBUG --startup_eosdash false --reload true uv run python -m akkudoktoreos.server.eos --host localhost --port 8503 --log_level DEBUG --startup_eosdash false --reload true
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
python -m akkudoktoreos.server.eos --host localhost --port 8503 --log_level DEBUG --startup_eosdash false --reload true uv run python -m akkudoktoreos.server.eos --host localhost --port 8503 --log_level DEBUG --startup_eosdash false --reload true
.. tab:: Linux Make .. tab:: Linux Make
@@ -381,13 +357,13 @@ At a minimum, you should run the module tests:
.. code-block:: powershell .. code-block:: powershell
pytest -vs --cov src --cov-report term-missing uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
pytest -vs --cov src --cov-report term-missing uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux Make .. tab:: Linux Make
@@ -413,13 +389,13 @@ resources:
.. code-block:: powershell .. code-block:: powershell
pytest --system-test -vs --cov src --cov-report term-missing uv run pytest --system-test -vs --cov src --cov-report term-missing
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
pytest --system-test -vs --cov src --cov-report term-missing uv run pytest --system-test -vs --cov src --cov-report term-missing
.. tab:: Linux Make .. tab:: Linux Make
@@ -437,13 +413,13 @@ To do profiling use:
.. code-block:: powershell .. code-block:: powershell
python tests/single_test_optimization.py --profile uv run python tests/single_test_optimization.py --profile
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
python tests/single_test_optimization.py --profile uv run python tests/single_test_optimization.py --profile
.. tab:: Linux Make .. tab:: Linux Make
@@ -575,13 +551,13 @@ Ensure your changes do not break existing functionality:
.. code-block:: powershell .. code-block:: powershell
pytest -vs --cov src --cov-report term-missing uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
pytest -vs --cov src --cov-report term-missing uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux Make .. tab:: Linux Make

View File

@@ -25,8 +25,8 @@ Before installing, ensure you have the following:
### For Source / Release Installation (M1/M2) ### For Source / Release Installation (M1/M2)
- Python 3.10 or higher - Python 3.11+
- pip - uv (recommended)
- Git (only for source) - Git (only for source)
- Tar/Zip (for release package) - Tar/Zip (for release package)
@@ -52,6 +52,24 @@ Akkudoktor-EOS is a [Home Assistant add-on](https://www.home-assistant.io/addons
have access to add-ons. have access to add-ons.
::: :::
## Install uv (one-time setup)
```{eval-rst}
.. tabs::
.. tab:: Windows
.. code-block:: powershell
irm https://astral.sh/uv/install.ps1 | iex
.. tab:: Linux
.. code-block:: bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
## Installation from Source (GitHub) (M1) ## Installation from Source (GitHub) (M1)
Recommended for developers or users wanting the latest updates. Recommended for developers or users wanting the latest updates.
@@ -85,17 +103,13 @@ Recommended for developers or users wanting the latest updates.
.. code-block:: powershell .. code-block:: powershell
python -m venv .venv uv sync --extra dev
.venv\Scripts\pip install -r requirements.txt
.venv\Scripts\pip install -e .
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
python -m venv .venv uv sync --extra dev
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e .
``` ```
@@ -108,13 +122,13 @@ Recommended for developers or users wanting the latest updates.
.. code-block:: powershell .. code-block:: powershell
.venv\Scripts\python -m akkudoktoreos.server.eos uv run python -m akkudoktoreos.server.eos
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
.venv/bin/python -m akkudoktoreos.server.eos uv run python -m akkudoktoreos.server.eos
``` ```
@@ -134,13 +148,13 @@ stage of the installation provide appropriate IP addresses on startup.
.. code-block:: powershell .. code-block:: powershell
.venv\Scripts\python -m akkudoktoreos.server.eos --host 0.0.0.0 --eosdash-host 0.0.0.0 uv run python -m akkudoktoreos.server.eos --host 0.0.0.0 --eosdash-host 0.0.0.0
.. tab:: Linux .. tab:: Linux
.. code-block:: bash .. code-block:: bash
.venv/bin/python -m akkudoktoreos.server.eos --host 0.0.0.0 --eosdash-host 0.0.0.0 uv run python -m akkudoktoreos.server.eos --host 0.0.0.0 --eosdash-host 0.0.0.0
``` ```
<!-- pyml enable line-length --> <!-- pyml enable line-length -->

View File

@@ -51,7 +51,7 @@ git checkout v0.1.0
Then reinstall dependencies: Then reinstall dependencies:
```bash ```bash
.venv/bin/pip install -r requirements.txt --upgrade uv sync
``` ```
#### Release package (M2) #### Release package (M2)
@@ -62,7 +62,7 @@ Refer to **Method 2** in the [Installation Guideline](install-page).
### 3) Restart EOS (M1/M2) ### 3) Restart EOS (M1/M2)
```bash ```bash
.venv/bin/python -m akkudoktoreos.server.eos uv run python -m akkudoktoreos.server.eos
``` ```
### 4) Restore configuration (optional) (M1/M2) ### 4) Restore configuration (optional) (M1/M2)

View File

@@ -8,7 +8,7 @@
"name": "Apache 2.0", "name": "Apache 2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0.html" "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}, },
"version": "v0.2.0.dev58204789" "version": "v0.2.0.dev2602231150315077"
}, },
"paths": { "paths": {
"/v1/admin/cache/clear": { "/v1/admin/cache/clear": {
@@ -4451,7 +4451,7 @@
"type": "string", "type": "string",
"title": "Version", "title": "Version",
"description": "Configuration file version. Used to check compatibility.", "description": "Configuration file version. Used to check compatibility.",
"default": "0.2.0.dev58204789" "default": "0.2.0.dev2602231150315077"
}, },
"data_folder_path": { "data_folder_path": {
"type": "string", "type": "string",
@@ -4514,7 +4514,7 @@
"type": "string", "type": "string",
"title": "Version", "title": "Version",
"description": "Configuration file version. Used to check compatibility.", "description": "Configuration file version. Used to check compatibility.",
"default": "0.2.0.dev58204789" "default": "0.2.0.dev2602231150315077"
}, },
"data_folder_path": { "data_folder_path": {
"type": "string", "type": "string",

View File

@@ -13,18 +13,87 @@ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [
"babel==2.18.0",
"beautifulsoup4==4.14.3",
"cachebox==5.2.2",
"numpy==2.4.2",
"numpydantic==1.7.0",
"matplotlib==3.10.8",
"contourpy==1.3.3",
"fastapi[standard-no-fastapi-cloud-cli]==0.128.0",
"fastapi_cli==0.0.21",
"rich-toolkit==0.19.4",
"python-fasthtml==0.12.47",
"MonsterUI==1.0.43",
"markdown-it-py==4.0.0",
"mdit-py-plugins==0.5.0",
"bokeh==3.8.2",
"uvicorn==0.40.0",
"scipy==1.17.0",
"tzfpy==1.1.1",
"deap==1.4.3",
"requests==2.32.5",
"pandas==3.0.0",
"pendulum==3.2.0",
"platformdirs==4.9.2",
"psutil==7.2.2",
"pvlib==0.15.0",
"pydantic==2.12.5",
"pydantic_extra_types==2.11.0",
"statsmodels==0.14.6",
"pydantic-settings==2.13.1",
"linkify-it-py==2.0.3",
"loguru==0.7.3",
"lmdb==1.7.5",
]
[project.optional-dependencies]
dev = [
# Pre-commit framework - basic package requirements handled by pre-commit itself
# - pre-commit-hooks
# - isort
# - ruff
# - mypy (mirrors-mypy) - sync with requirements-dev.txt (if on pypi)
# - pymarkdown
# - commitizen - sync with requirements-dev.txt (if on pypi)
#
# !!! Sync .pre-commit-config.yaml with these dependencies !!!
"pre-commit==4.5.1",
"mypy==1.19.1",
"types-requests==2.32.4.20260107", # for mypy
"pandas-stubs==3.0.0.260204", # for mypy
"tokenize-rt==6.2.0", # for mypy
"types-docutils==0.22.3.20251115", # for mypy
"types-PyYaml==6.0.12.20250915", # for mypy
"commitizen==4.13.8",
"deprecated==1.3.1", # for commitizen
# Sphinx
"sphinx==9.0.4",
"sphinx_rtd_theme==3.1.0",
"sphinx-tabs==3.4.7",
"GitPython==3.1.46",
"myst-parser==5.0.0",
"docutils==0.21.2",
# Pytest
"pytest==9.0.2",
"pytest-asyncio==1.3.0",
"pytest-cov==7.0.0",
"pytest-xprocess==1.0.2",
"coverage==7.13.4",
]
[project.urls] [project.urls]
Homepage = "https://github.com/Akkudoktor-EOS/EOS" Homepage = "https://github.com/Akkudoktor-EOS/EOS"
Issues = "https://github.com/Akkudoktor-EOS/EOS/issues" Issues = "https://github.com/Akkudoktor-EOS/EOS/issues"
[build-system] [build-system]
requires = ["setuptools>=61.0"] requires = ["setuptools>=77", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
optional-dependencies = {dev = { file = ["requirements-dev.txt"] }}
# version.txt must be generated # version.txt must be generated
version = { file = "version.txt" } version = { file = "version.txt" }
@@ -92,6 +161,7 @@ markers = [
] ]
[tool.mypy] [tool.mypy]
mypy_path= "src"
files = ["src", "tests"] files = ["src", "tests"]
exclude = "class_soc_calc\\.py$" exclude = "class_soc_calc\\.py$"
check_untyped_defs = true check_untyped_defs = true

View File

@@ -1,35 +0,0 @@
-r requirements.txt
# Pre-commit framework - basic package requirements handled by pre-commit itself
# - pre-commit-hooks
# - isort
# - ruff
# - mypy (mirrors-mypy) - sync with requirements-dev.txt (if on pypi)
# - pymarkdown
# - commitizen - sync with requirements-dev.txt (if on pypi)
#
# !!! Sync .pre-commit-config.yaml and requirements-dev.txt !!!
pre-commit==4.5.1
mypy==1.19.1
types-requests==2.32.4.20260107 # for mypy
pandas-stubs==3.0.0.260204 # for mypy
tokenize-rt==6.2.0 # for mypy
types-docutils==0.22.3.20251115 # for mypy
types-PyYaml==6.0.12.20250915 # for mypy
commitizen==4.13.8
deprecated==1.3.1 # for commitizen
# Sphinx
sphinx==9.0.4
sphinx_rtd_theme==3.1.0
sphinx-tabs==3.4.7
GitPython==3.1.46
myst-parser==5.0.0
docutils==0.21.2
# Pytest
pytest==9.0.2
pytest-asyncio==1.3.0
pytest-cov==7.0.0
pytest-xprocess==1.0.2
coverage==7.13.4

View File

@@ -1,32 +0,0 @@
babel==2.18.0
beautifulsoup4==4.14.3
cachebox==5.2.2
numpy==2.4.2
numpydantic==1.7.0
matplotlib==3.10.8
contourpy==1.3.3
fastapi[standard-no-fastapi-cloud-cli]==0.128.0
fastapi_cli==0.0.21
rich-toolkit==0.19.4
python-fasthtml==0.12.47
MonsterUI==1.0.43
markdown-it-py==4.0.0
mdit-py-plugins==0.5.0
bokeh==3.8.2
uvicorn==0.40.0
scipy==1.17.0
tzfpy==1.1.1
deap==1.4.3
requests==2.32.5
pandas==3.0.0
pendulum==3.2.0
platformdirs==4.9.2
psutil==7.2.2
pvlib==0.15.0
pydantic==2.12.5
pydantic_extra_types==2.11.0
statsmodels==0.14.6
pydantic-settings==2.13.1
linkify-it-py==2.0.3
loguru==0.7.3
lmdb==1.7.5

42
scripts/cz_check_branch.py Normal file → Executable file
View File

@@ -11,16 +11,40 @@ import sys
from pathlib import Path from pathlib import Path
def find_cz() -> str: def find_cz() -> list[str]:
venv = os.getenv("VIRTUAL_ENV") """Return command to invoke Commitizen via virtualenv or globally."""
paths = [Path(venv)] if venv else [] candidates = []
paths.append(Path.cwd() / ".venv")
for base in paths: # 1⃣ Currently active virtualenv
cz = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz") venv = os.getenv("VIRTUAL_ENV")
if cz.exists(): if venv:
return str(cz) candidates.append(Path(venv))
return "cz"
# 2⃣ uv-managed virtualenv
uv_venv = Path(".uv") / "venv"
if uv_venv.exists():
candidates.append(uv_venv)
# 3⃣ traditional .venv
dot_venv = Path(".venv")
if dot_venv.exists():
candidates.append(dot_venv)
# Check each candidate for Commitizen binary
for base in candidates:
cz_path = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
if cz_path.exists():
return [str(cz_path)]
# 4⃣ fallback to uv run cz
try:
subprocess.run(["uv", "run", "cz", "--version"], check=True, stdout=subprocess.DEVNULL)
return ["uv", "run", "cz"]
except (subprocess.CalledProcessError, FileNotFoundError):
pass
# 5⃣ fallback to system cz
return ["cz"]
def main(): def main():

53
scripts/cz_check_commit_message.py Normal file → Executable file
View File

@@ -2,6 +2,12 @@
"""Commitizen commit message checker that is .venv aware. """Commitizen commit message checker that is .venv aware.
Works for commits with -m or commit message file. Works for commits with -m or commit message file.
Cross-platform + uv/.venv aware:
- Prefers activated virtual environment (VIRTUAL_ENV)
- Falls back to uv-managed .uv/venv
- Falls back to .venv
- Falls back to global cz
""" """
import os import os
@@ -10,19 +16,40 @@ import sys
from pathlib import Path from pathlib import Path
def find_cz() -> str: def find_cz() -> list[str]:
"""Find Commitizen executable, preferring virtualenv.""" """Return command to invoke Commitizen via virtualenv or globally."""
venv = os.getenv("VIRTUAL_ENV") candidates = []
paths = []
if venv:
paths.append(Path(venv))
paths.append(Path.cwd() / ".venv")
for base in paths: # 1⃣ Currently active virtualenv
cz = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz") venv = os.getenv("VIRTUAL_ENV")
if cz.exists(): if venv:
return str(cz) candidates.append(Path(venv))
return "cz"
# 2⃣ uv-managed virtualenv
uv_venv = Path(".uv") / "venv"
if uv_venv.exists():
candidates.append(uv_venv)
# 3⃣ traditional .venv
dot_venv = Path(".venv")
if dot_venv.exists():
candidates.append(dot_venv)
# Check each candidate for Commitizen binary
for base in candidates:
cz_path = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
if cz_path.exists():
return [str(cz_path)]
# 4⃣ fallback to uv run cz
try:
subprocess.run(["uv", "run", "cz", "--version"], check=True, stdout=subprocess.DEVNULL)
return ["uv", "run", "cz"]
except (subprocess.CalledProcessError, FileNotFoundError):
pass
# 5⃣ fallback to system cz
return ["cz"]
def main(): def main():
@@ -47,7 +74,7 @@ def main():
print(f"🔍 Checking commit message using {cz}...") print(f"🔍 Checking commit message using {cz}...")
try: try:
subprocess.check_call([cz, "check", "--commit-msg-file", commit_msg_file]) subprocess.check_call(cz + ["check", "--commit-msg-file", commit_msg_file])
print("✅ Commit message follows Commitizen convention.") print("✅ Commit message follows Commitizen convention.")
return 0 return 0
except subprocess.CalledProcessError: except subprocess.CalledProcessError:

58
scripts/cz_check_new_commits.py Normal file → Executable file
View File

@@ -1,10 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Pre-push hook: Commitizen check for *new commits only*. """Pre-push hook: Commitizen check for *new commits only*.
Cross-platform + virtualenv-aware: Cross-platform + uv/.venv aware:
- Prefers activated virtual environment (VIRTUAL_ENV) - Prefers activated virtual environment (VIRTUAL_ENV)
- Falls back to ./.venv if found - Falls back to uv-managed .uv/venv
- Falls back to global cz otherwise - Falls back to .venv
- Falls back to global cz
""" """
import os import os
@@ -13,23 +14,40 @@ import sys
from pathlib import Path from pathlib import Path
def find_cz_executable() -> str: def find_cz() -> list[str]:
"""Return path to Commitizen executable, preferring virtual environments.""" """Return command to invoke Commitizen via virtualenv or globally."""
# 1⃣ Active virtual environment (if running inside one) candidates = []
venv_env = os.getenv("VIRTUAL_ENV")
if venv_env: # 1⃣ Currently active virtualenv
cz_path = Path(venv_env) / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz") venv = os.getenv("VIRTUAL_ENV")
if venv:
candidates.append(Path(venv))
# 2⃣ uv-managed virtualenv
uv_venv = Path(".uv") / "venv"
if uv_venv.exists():
candidates.append(uv_venv)
# 3⃣ traditional .venv
dot_venv = Path(".venv")
if dot_venv.exists():
candidates.append(dot_venv)
# Check each candidate for Commitizen binary
for base in candidates:
cz_path = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
if cz_path.exists(): if cz_path.exists():
return str(cz_path) return [str(cz_path)]
# 2️⃣ Local .venv in repo root # 4️⃣ fallback to uv run cz
repo_venv = Path.cwd() / ".venv" try:
cz_path = repo_venv / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz") subprocess.run(["uv", "run", "cz", "--version"], check=True, stdout=subprocess.DEVNULL)
if cz_path.exists(): return ["uv", "run", "cz"]
return str(cz_path) except (subprocess.CalledProcessError, FileNotFoundError):
pass
# 3️⃣ Global fallback # 5️⃣ fallback to system cz
return "cz" return ["cz"]
def get_merge_base() -> str | None: def get_merge_base() -> str | None:
@@ -48,17 +66,17 @@ def get_merge_base() -> str | None:
def main() -> int: def main() -> int:
cz = find_cz_executable() cz = find_cz()
base = get_merge_base() base = get_merge_base()
if not base: if not base:
print("⚠️ No upstream found; skipping Commitizen check for new commits.") print("⚠️ No upstream found; skipping Commitizen check {cz} for new commits.")
return 0 return 0
print(f"🔍 Using {cz} to check new commits from {base}..HEAD ...") print(f"🔍 Using {cz} to check new commits from {base}..HEAD ...")
try: try:
subprocess.check_call([cz, "check", "--rev-range", f"{base}..HEAD"]) subprocess.check_call(cz + ["check", "--rev-range", f"{base}..HEAD"])
print("✅ All new commits follow Commitizen conventions.") print("✅ All new commits follow Commitizen conventions.")
return 0 return 0
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:

View File

@@ -14,6 +14,11 @@ from loguru import logger
from pydantic.fields import ComputedFieldInfo, FieldInfo from pydantic.fields import ComputedFieldInfo, FieldInfo
from pydantic_core import PydanticUndefined from pydantic_core import PydanticUndefined
# Add the src directory to sys.path so import akkudoktoreos works in all cases
PROJECT_ROOT = Path(__file__).parent.parent
SRC_DIR = PROJECT_ROOT / "src"
sys.path.insert(0, str(SRC_DIR))
from akkudoktoreos.config.config import ConfigEOS, default_data_folder_path from akkudoktoreos.config.config import ConfigEOS, default_data_folder_path
from akkudoktoreos.core.coreabc import get_config, singletons_init from akkudoktoreos.core.coreabc import get_config, singletons_init
from akkudoktoreos.core.pydantic import PydanticBaseModel from akkudoktoreos.core.pydantic import PydanticBaseModel

View File

@@ -18,6 +18,12 @@ import argparse
import json import json
import os import os
import sys import sys
from pathlib import Path
# Add the src directory to sys.path so import akkudoktoreos works in all cases
PROJECT_ROOT = Path(__file__).parent.parent
SRC_DIR = PROJECT_ROOT / "src"
sys.path.insert(0, str(SRC_DIR))
from akkudoktoreos.core.coreabc import get_config from akkudoktoreos.core.coreabc import get_config
from akkudoktoreos.server.eos import app from akkudoktoreos.server.eos import app

View File

@@ -1,15 +1,29 @@
#!.venv/bin/python
"""Get version of EOS""" """Get version of EOS"""
import sys import sys
if sys.version_info < (3, 11):
print(
f"ERROR: Python >=3.11 is required. Found {sys.version_info.major}.{sys.version_info.minor}",
file=sys.stderr,
)
sys.exit(1)
from pathlib import Path from pathlib import Path
# Add the src directory to sys.path so Sphinx can import akkudoktoreos # Add the src directory to sys.path so import akkudoktoreos works in all cases
PROJECT_ROOT = Path(__file__).parent.parent PROJECT_ROOT = Path(__file__).parent.parent
SRC_DIR = PROJECT_ROOT / "src" SRC_DIR = PROJECT_ROOT / "src"
sys.path.insert(0, str(SRC_DIR)) sys.path.insert(0, str(SRC_DIR))
from akkudoktoreos.core.version import __version__
if __name__ == "__main__": if __name__ == "__main__":
print(__version__) # Import here to prevent mypy to execute the functions that evaluate __version__
try:
from akkudoktoreos.core.version import __version__
version = __version__
except Exception:
# This may be a first time install
raise RuntimeError("Can not find out version!")
version = "0.0.0"
print(version)

View File

@@ -11,6 +11,12 @@ import sys
from pathlib import Path from pathlib import Path
from typing import List from typing import List
# Add the src directory to sys.path so import akkudoktoreos works in all cases
PROJECT_ROOT = Path(__file__).parent.parent
SRC_DIR = PROJECT_ROOT / "src"
sys.path.insert(0, str(SRC_DIR))
# --- Patterns to match version strings --- # --- Patterns to match version strings ---
VERSION_PATTERNS = [ VERSION_PATTERNS = [
# Python: __version__ = "1.2.3" # Python: __version__ = "1.2.3"
@@ -88,6 +94,21 @@ def update_version_in_file(file_path: Path, new_version: str) -> bool:
return file_would_be_updated return file_would_be_updated
def update_version_date_file() -> str:
"""Write current version date to __version_date__.py"""
from akkudoktoreos.core.version import VERSION_DATE_FILE, _version_date_hash
version_date, _ = _version_date_hash()
version_date_str = version_date.strftime('%Y-%m-%dT%H:%M:%SZ')
content = f'VERSION_DATE = "{version_date_str}"\n'
VERSION_DATE_FILE.write_text(content)
print(f"Updated {VERSION_DATE_FILE} with UTC date {version_date_str}")
return str(VERSION_DATE_FILE)
def main(version: str, files: List[str]): def main(version: str, files: List[str]):
if not version: if not version:
raise ValueError("No version provided") raise ValueError("No version provided")
@@ -103,6 +124,8 @@ def main(version: str, files: List[str]):
if update_version_in_file(path, version): if update_version_in_file(path, version):
updated_files.append(str(path)) updated_files.append(str(path))
updated_files.append(update_version_date_file())
if updated_files: if updated_files:
print(f"Updated files: {', '.join(updated_files)}") print(f"Updated files: {', '.join(updated_files)}")
else: else:

View File

@@ -0,0 +1 @@
VERSION_DATE = "2026-02-23T11:41:01Z"

View File

@@ -1,8 +1,21 @@
"""Version information for akkudoktoreos.""" """Version information for akkudoktoreos."""
# -----------------------------------------------------------------
# version.py may be used __BEFORE__ the dependencies are installed.
# Use only standard python libraries
#
# Several warnings/ erros are silenced because they are
# non-critical in this context - see noqa.
# -----------------------------------------------------------------
import hashlib import hashlib
import re import re
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from datetime import ( # Don't use akkudoktoreos.utils.datetimeutil (-> pendulum)
datetime,
timezone,
)
from fnmatch import fnmatch from fnmatch import fnmatch
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -14,14 +27,18 @@ VERSION_BASE = "0.2.0.dev"
# Project hash of relevant files # Project hash of relevant files
HASH_EOS = "" HASH_EOS = ""
# Number of digits to append to .dev to identify a development version # Number of hash digits to append to .dev to identify a development version
VERSION_DEV_PRECISION = 8 VERSION_DEV_HASH_PRECISION = 8
# File to hold the date of the latest commit.
VERSION_DATE_FILE = Path(__file__).parent / "_version_date.py"
# Hashing configuration # Hashing configuration
DIR_PACKAGE_ROOT = Path(__file__).resolve().parent.parent DIR_PACKAGE_ROOT = Path(__file__).resolve().parent.parent
ALLOWED_SUFFIXES: set[str] = {".py", ".md", ".json"} ALLOWED_SUFFIXES: set[str] = {".py", ".md", ".json"}
EXCLUDED_DIR_PATTERNS: set[str] = {"*_autosum", "*__pycache__", "*_generated"} EXCLUDED_DIR_PATTERNS: set[str] = {"*_autosum", "*__pycache__", "*_generated"}
EXCLUDED_FILES: set[Path] = set() # Excluded from hash/date calculation to avoid self-referencing loop
EXCLUDED_FILES: set[Path] = {VERSION_DATE_FILE}
# ------------------------------ # ------------------------------
@@ -169,18 +186,78 @@ def hash_tree(
return digest, files return digest, files
def newest_commit_or_dirty_datetime(files: list[Path]) -> datetime:
"""Return the newest relevant datetime for the given files in UTC.
Checks for uncommitted changes among the given files first. If any file
has staged or unstaged modifications, the current UTC datetime is returned
to reflect that the working tree is ahead of the last commit. Otherwise,
the datetime of the most recent git commit touching any of the given files
is returned. If git is unavailable (e.g. after pip install), falls back to
reading the date from VERSION_DATE_FILE.
Args:
files: List of file paths to check for changes and commit history.
Returns:
The current UTC datetime if any file has uncommitted changes, otherwise
the UTC datetime of the most recent commit touching any of the given
files, or the datetime stored in VERSION_DATE_FILE as a last resort.
Raises:
RuntimeError: If no version date can be determined from any source.
"""
# Check for uncommitted changes among watched files
try:
status_result = subprocess.run( # noqa: S603
["git", "status", "--porcelain", "--"] + [str(f) for f in files],
capture_output=True,
text=True,
check=True,
cwd=DIR_PACKAGE_ROOT,
)
if status_result.stdout.strip():
return datetime.now(tz=timezone.utc)
result = subprocess.run( # noqa: S603
["git", "log", "-1", "--format=%ct", "--"] + [str(f) for f in files],
capture_output=True,
text=True,
check=True,
cwd=DIR_PACKAGE_ROOT,
)
ts = result.stdout.strip()
if ts:
return datetime.fromtimestamp(int(ts), tz=timezone.utc)
except (subprocess.CalledProcessError, FileNotFoundError, ValueError): # noqa: S110
pass
# Fallback to VERSION_DATE_FILE
if VERSION_DATE_FILE.exists():
try:
ns: dict[str, str] = {}
exec(VERSION_DATE_FILE.read_text(), {}, ns) # noqa: S102
date_str = ns.get("VERSION_DATE")
if date_str:
return datetime.fromisoformat(date_str).astimezone(timezone.utc)
except Exception: # noqa: S110
pass
raise RuntimeError("This should not happen - No version date info available")
# --------------------- # ---------------------
# Version hash function # Version hash function
# --------------------- # ---------------------
def _version_hash() -> str: def _version_date_hash() -> tuple[datetime, str]:
"""Calculate project hash. """Calculate project date and hash.
Only package files in src/akkudoktoreos can be hashed to make it work also for packages. Only package files in src/akkudoktoreos can be hashed to make it work also for packages.
Returns: Returns:
SHA256 hash of the project files lattest commit date and SHA256 hash of the project files
""" """
if not str(DIR_PACKAGE_ROOT).endswith("src/akkudoktoreos"): if not str(DIR_PACKAGE_ROOT).endswith("src/akkudoktoreos"):
error_msg = f"DIR_PACKAGE_ROOT does not end with src/akkudoktoreos: {DIR_PACKAGE_ROOT}" error_msg = f"DIR_PACKAGE_ROOT does not end with src/akkudoktoreos: {DIR_PACKAGE_ROOT}"
@@ -197,23 +274,29 @@ def _version_hash() -> str:
excluded_files=EXCLUDED_FILES, excluded_files=EXCLUDED_FILES,
) )
return hash_digest date = newest_commit_or_dirty_datetime(hashed_files)
return date, hash_digest
def _version_calculate() -> str: def _version_calculate() -> str:
"""Calculate the full version string. """Calculate the full version string.
For release versions: "x.y.z" For release versions: "x.y.z"
For dev versions: "x.y.z.dev<hash>" For dev versions: "x.y.z.dev<date><hash>"
Returns: Returns:
Full version string Full version string
""" """
if VERSION_BASE.endswith(".dev"): if VERSION_BASE.endswith(".dev"):
# After dev only digits are allowed - convert hexdigest to digits # After dev only digits are allowed - convert hexdigest to digits
hash_value = int(_version_hash(), 16) version_date, version_hash = _version_date_hash()
hash_digits = str(hash_value % (10**VERSION_DEV_PRECISION)).zfill(VERSION_DEV_PRECISION) hash_value = int(version_hash, 16)
return f"{VERSION_BASE}{hash_digits}" hash_digits = str(hash_value % (10**VERSION_DEV_HASH_PRECISION)).zfill(
VERSION_DEV_HASH_PRECISION
)
date_digits = version_date.strftime("%y%m%d%H") if version_date else "00000000"
return f"{VERSION_BASE}{date_digits}{hash_digits}"
else: else:
# Release version - use base as-is # Release version - use base as-is
return VERSION_BASE return VERSION_BASE
@@ -234,10 +317,10 @@ __version__ = _version_calculate()
# Regular expression to split the version string into pieces # Regular expression to split the version string into pieces
VERSION_RE = re.compile( VERSION_RE = re.compile(
r""" r"""
^(?P<base>\d+\.\d+\.\d+) # x.y.z ^(?P<base>\d+\.\d+\.\d+) # x.y.z
(?:\. # .dev<hash> starts here (?:\.dev # literal '.dev' for development versions
(?P<dev>dev) # literal 'dev' (?P<date>\d{8}) # 8-digit date: YYMMDDHH
(?P<hash>[a-f0-9]+)? # optional <hash> (hex digits) (?P<hash>[a-f0-9]+)? # hex hash
)? )?
$ $
""", """,
@@ -251,7 +334,7 @@ def version() -> dict[str, Optional[str]]:
The version string shall be of the form: The version string shall be of the form:
x.y.z x.y.z
x.y.z.dev x.y.z.dev
x.y.z.dev<HASH> x.y.z.dev<date><hash>
Returns: Returns:
.. code-block:: python .. code-block:: python

View File

@@ -24,7 +24,7 @@ from xprocess import ProcessStarter, XProcess
from akkudoktoreos.config.config import ConfigEOS from akkudoktoreos.config.config import ConfigEOS
from akkudoktoreos.core.coreabc import get_config, get_prediction, singletons_init from akkudoktoreos.core.coreabc import get_config, get_prediction, singletons_init
from akkudoktoreos.core.version import _version_hash, version from akkudoktoreos.core.version import _version_date_hash, version
from akkudoktoreos.server.server import get_default_host from akkudoktoreos.server.server import get_default_host
# ----------------------------------------------- # -----------------------------------------------
@@ -510,26 +510,41 @@ def server_base(
if extra_env: if extra_env:
env.update(extra_env) env.update(extra_env)
# assure server to be installed project_dir = Path(__file__).parent.parent
try:
project_dir = Path(__file__).parent.parent @staticmethod
subprocess.run( def _ensure_package(env: dict, project_dir: Path) -> None:
[sys.executable, "-c", "import", "akkudoktoreos.server.eos"], """Ensure 'akkudoktoreos' is importable in this Python environment."""
check=True, try:
env=env, subprocess.run(
stdout=subprocess.PIPE, [sys.executable, "-c", "import akkudoktoreos.server.eos"],
stderr=subprocess.PIPE, check=True,
cwd=project_dir, env=env,
) stdout=subprocess.PIPE,
except subprocess.CalledProcessError: stderr=subprocess.PIPE,
subprocess.run( cwd=project_dir,
[sys.executable, "-m", "pip", "install", "-e", str(project_dir)], )
env=env, except subprocess.CalledProcessError:
check=True, # If inside a normal venv or uv-managed environment, install in place
stdout=subprocess.PIPE, uv_root = os.getenv("UV_VENV_ROOT") # set by uv if active
stderr=subprocess.PIPE, venv_active = hasattr(sys, "real_prefix") or sys.prefix != sys.base_prefix
cwd=project_dir, if uv_root or venv_active:
) print("Package not found, installing in current environment...")
subprocess.run(
[sys.executable, "-m", "pip", "install", "-e", str(project_dir)],
check=True,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=project_dir,
)
else:
raise RuntimeError(
"Cannot import 'akkudoktoreos.server.eos' in the system Python. "
"Activate a virtual environment first."
)
_ensure_package(env, project_dir)
# Set command to start server process # Set command to start server process
args = [ args = [
@@ -644,7 +659,7 @@ def version_and_hash() -> Generator[dict[str, Optional[str]], None, None]:
Runs once per test session. Runs once per test session.
""" """
info = version() info = version()
info["hash_current"] = _version_hash() _, info["hash_current"] = _version_date_hash()
yield info yield info

View File

@@ -14,10 +14,11 @@ from akkudoktoreos.core.version import (
EXCLUDED_FILES, EXCLUDED_FILES,
HashConfig, HashConfig,
_version_calculate, _version_calculate,
_version_hash, _version_date_hash,
collect_files, collect_files,
hash_files, hash_files,
) )
from akkudoktoreos.utils.datetimeutil import to_datetime
DIR_PROJECT_ROOT = Path(__file__).parent.parent DIR_PROJECT_ROOT = Path(__file__).parent.parent
GET_VERSION_SCRIPT = DIR_PROJECT_ROOT / "scripts" / "get_version.py" GET_VERSION_SCRIPT = DIR_PROJECT_ROOT / "scripts" / "get_version.py"
@@ -127,7 +128,7 @@ def write_file(path: Path, content: str):
# -- Test version calculation --- # -- Test version calculation ---
def test_version_hash() -> None: def test_version_date_hash() -> None:
"""Test which files are used for version hash calculation.""" """Test which files are used for version hash calculation."""
watched_paths = [DIR_PACKAGE_ROOT] watched_paths = [DIR_PACKAGE_ROOT]
@@ -196,22 +197,26 @@ def test_version_non_dev(monkeypatch):
def test_version_dev_precision_8(monkeypatch): def test_version_dev_precision_8(monkeypatch):
"""Test that a dev version appends exactly 8 digits derived from the hash.""" """Test that a dev version appends exactly 8 digits derived from the hash."""
fake_hash = "abcdef1234567890" # deterministic fake digest fake_hash = "abcdef1234567890"
fake_date = "2025-02-22T09:28:22Z"
fake_date_hash = (to_datetime(fake_date), fake_hash) # deterministic fake digest
monkeypatch.setattr("akkudoktoreos.core.version._version_hash", lambda: fake_hash) monkeypatch.setattr("akkudoktoreos.core.version._version_date_hash", lambda: fake_date_hash)
monkeypatch.setattr("akkudoktoreos.core.version.VERSION_BASE", "0.2.0.dev") monkeypatch.setattr("akkudoktoreos.core.version.VERSION_BASE", "0.2.0.dev")
monkeypatch.setattr("akkudoktoreos.core.version.VERSION_DEV_PRECISION", 8) monkeypatch.setattr("akkudoktoreos.core.version.VERSION_DEV_HASH_PRECISION", 8)
result = _version_calculate() result = _version_calculate()
# Compute expected suffix using the same logic as _version_calculate # Compute expected suffix using the same logic as _version_calculate
hash_value = int(fake_hash, 16) hash_value = int(fake_hash, 16)
expected_digits = str(hash_value % (10 ** 8)).zfill(8) expected_hash_digits = str(hash_value % (10 ** 8)).zfill(8)
expected = f"0.2.0.dev{expected_digits}" expected_date_digits = to_datetime(fake_date, as_string="YYMMDDHH")
expected = f"0.2.0.dev{expected_date_digits}{expected_hash_digits}"
assert result == expected assert result == expected
assert len(expected_digits) == 8 assert len(expected_hash_digits) == 8
assert result.startswith("0.2.0.dev") assert result.startswith("0.2.0.dev")
assert result == expected assert result == expected
@@ -219,21 +224,25 @@ def test_version_dev_precision_8(monkeypatch):
def test_version_dev_precision_8_different_hash(monkeypatch): def test_version_dev_precision_8_different_hash(monkeypatch):
"""A different hash must produce a different 8-digit suffix.""" """A different hash must produce a different 8-digit suffix."""
fake_hash = "1234abcd9999ffff" fake_hash = "1234abcd9999ffff"
fake_date = "2025-02-22T09:28:22Z"
fake_date_hash = (to_datetime(fake_date), fake_hash) # deterministic fake digest
monkeypatch.setattr("akkudoktoreos.core.version._version_hash", lambda: fake_hash) monkeypatch.setattr("akkudoktoreos.core.version._version_date_hash", lambda: fake_date_hash)
monkeypatch.setattr("akkudoktoreos.core.version.VERSION_BASE", "0.2.0.dev") monkeypatch.setattr("akkudoktoreos.core.version.VERSION_BASE", "0.2.0.dev")
monkeypatch.setattr("akkudoktoreos.core.version.VERSION_DEV_PRECISION", 8) monkeypatch.setattr("akkudoktoreos.core.version.VERSION_DEV_HASH_PRECISION", 8)
result = _version_calculate() result = _version_calculate()
# Compute expected suffix using the same logic as _version_calculate # Compute expected suffix using the same logic as _version_calculate
hash_value = int(fake_hash, 16) hash_value = int(fake_hash, 16)
expected_digits = str(hash_value % (10 ** 8)).zfill(8) expected_hash_digits = str(hash_value % (10 ** 8)).zfill(8)
expected = f"0.2.0.dev{expected_digits}" expected_date_digits = to_datetime(fake_date, as_string="YYMMDDHH")
expected = f"0.2.0.dev{expected_date_digits}{expected_hash_digits}"
assert result == expected assert result == expected
assert len(expected_digits) == 8 assert len(expected_hash_digits) == 8
assert result.startswith("0.2.0.dev") assert result.startswith("0.2.0.dev")
assert result == expected assert result == expected

3427
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff