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
# -----------------------------------------------------------------------------
VERSION=0.2.0.dev58204789
VERSION=config.yaml
PYTHON_VERSION=3.13.9
# -----------------------------------------------------------------------------

View File

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

View File

@@ -31,14 +31,14 @@ repos:
# --- Static type checking ---
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
rev: v1.19.1
hooks:
- id: mypy
additional_dependencies:
- types-requests==2.32.4.20250913
- pandas-stubs==2.3.2.250926
- pandas-stubs==3.0.0.260204
- tokenize-rt==6.2.0
- types-docutils==0.22.2.20251006
- types-docutils==0.22.3.20251115
- types-PyYaml==6.0.12.20250915
pass_filenames: false
@@ -59,23 +59,29 @@ repos:
# Validate commit messages (using Python wrapper)
- id: commitizen-commit
name: Commitizen (venv-aware)
entry: python3 scripts/cz_check_commit_message.py
language: system
entry: scripts/cz_check_commit_message.py
language: python
additional_dependencies:
- .
stages: [commit-msg]
pass_filenames: false
# Branch name check on push (using Python wrapper)
- id: commitizen-branch
name: Commitizen branch check
entry: python3 scripts/cz_check_branch.py
language: system
entry: scripts/cz_check_branch.py
language: python
additional_dependencies:
- .
stages: [pre-push]
pass_filenames: false
# Validate new commit messages before push (using Python wrapper)
- id: commitizen-new-commits
name: Commitizen (check new commits only, .venv aware)
entry: python3 -m scripts.cz_check_new_commits
language: system
entry: scripts/cz_check_new_commits.py
language: python
additional_dependencies:
- .
stages: [pre-push]
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
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,
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.
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.
- development version scheme
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
is appended by .dev as expected by docker.
- 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 VSCode devcontainer and task definition for home assistant add-on
development.
- Use uv to manage the virtual environment for development.
This enormously increases dependency updates.
- improve documentation
## 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 \
&& rm -rf /var/lib/apt/lists/*
# - Copy project metadata first (better Docker layer caching)
COPY pyproject.toml .
# - Create venv
RUN python3 -m venv ${VENV_PATH}
# - Upgrade pip inside venv
RUN pip install --upgrade pip setuptools wheel
# - Install deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install EOS/ EOSdash
# - Copy source
COPY src/ ./src
COPY pyproject.toml .
# - Create version information
COPY scripts/get_version.py ./scripts/get_version.py

123
Makefile
View File

@@ -1,8 +1,18 @@
# 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
VERSION := $(shell python3 scripts/get_version.py)
VERSION := $(shell $(PYTHON) scripts/get_version.py)
# Default target
all: help
@@ -10,13 +20,11 @@ all: help
# Target to display help information
help:
@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 " gitlint - Lint last commit message."
@echo " mypy - Run mypy."
@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-build - Rebuild docker image"
@echo " docs - Generate HTML documentation (in build/docs/html/)."
@@ -36,57 +44,48 @@ help:
@echo " clean - Remove generated documentation, distribution and virtual environment."
@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
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.
install: pip-dev version-txt
.venv/bin/pip install build
.venv/bin/pip install -e .
@echo "EOS installed in editable form (development mode)."
install: version-txt
# Upgrade installation and dependencies
$(UV) sync --extra dev
@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.
dist: pip
.venv/bin/pip install build
.venv/bin/python -m build --wheel
dist: version-txt
$(PIP) install build
$(PYTHON) -m build --wheel
@echo "Distribution created (see dist/)."
# Target to generate documentation
gen-docs: pip-dev version-txt
.venv/bin/pip install -e .
.venv/bin/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
.venv/bin/python ./scripts/generate_openapi.py --output-file openapi.json
gen-docs: version-txt
$(PYTHON) ./scripts/generate_config_md.py --output-file docs/_generated/config.md
$(PYTHON) ./scripts/generate_openapi_md.py --output-file docs/_generated/openapi.md
$(PYTHON) ./scripts/generate_openapi.py --output-file openapi.json
@echo "Documentation generated to openapi.json and docs/_generated."
# Target to build HTML documentation
docs: pip-dev
.venv/bin/pytest --finalize tests/test_docsphinx.py
docs: install
$(PYTEST) --finalize tests/test_docsphinx.py
@echo "Documentation build to build/docs/html/."
# Target to read the HTML documentation
read-docs:
@echo "Read the documentation in your browser"
.venv/bin/pytest --finalize tests/test_docsphinx.py
.venv/bin/python -m webbrowser build/docs/html/index.html
$(PYTEST) --finalize tests/test_docsphinx.py
$(PYTHON) -m webbrowser build/docs/html/index.html
# Clean Python bytecode
clean-bytecode:
@@ -104,64 +103,66 @@ clean-docs:
clean: clean-docs
@echo "Cleaning virtual env, distribution and build directories"
rm -rf build .venv
@echo "Cleaning uv environment"
$(UV) clean
@echo "Deletion complete."
run:
@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:
@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:
@echo "Starting EOSdash production server, please wait..."
.venv/bin/python -m akkudoktoreos.server.eosdash
$(PYTHON) -m akkudoktoreos.server.eosdash
run-dash-dev:
@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.
test-setup: pip-dev
test-setup: install
@echo "Setup tests"
# Target to run tests.
test:
@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.
test-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.
test-system:
@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.
test-finalize:
@echo "Running all tests..."
.venv/bin/pytest --finalize
$(PYTEST) --finalize
# Target to run tests including the single test optimization with profiling.
test-profile:
@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.
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:
.venv/bin/cz check --rev-range main..HEAD
$(COMMITIZEN) check --rev-range main..HEAD
# Target to format code.
mypy:
.venv/bin/mypy
$(MYPY)
# Run entire setup on docker
docker-run:
@@ -180,15 +181,15 @@ docker-build:
# Take UPDATE_FILES from GitHub action bump-version.yml
UPDATE_FILES := $(shell sed -n 's/^[[:space:]]*UPDATE_FILES[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \
.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"
.venv/bin/python ./scripts/update_version.py $(VERSION) $(UPDATE_FILES)
.venv/bin/python ./scripts/convert_lightweight_tags.py
.venv/bin/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
.venv/bin/python ./scripts/generate_openapi.py --output-file openapi.json
.venv/bin/pytest -vv --finalize tests/test_version.py
$(PYTHON) ./scripts/update_version.py $(VERSION) $(UPDATE_FILES)
$(PYTHON) ./scripts/convert_lightweight_tags.py
$(PYTHON) ./scripts/generate_config_md.py --output-file docs/_generated/config.md
$(PYTHON) ./scripts/generate_openapi_md.py --output-file docs/_generated/openapi.md
$(PYTHON) ./scripts/generate_openapi.py --output-file openapi.json
$(PYTEST) -vv --finalize tests/test_version.py
test-version:
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).
name: "Akkudoktor-EOS"
version: "0.2.0.dev58204789"
version: "0.2.0.dev2602231150315077"
slug: "eos"
description: "Akkudoktor-EOS add-on"
url: "https://github.com/Akkudoktor-EOS/EOS"

View File

@@ -120,7 +120,7 @@
}
},
"general": {
"version": "0.2.0.dev58204789",
"version": "0.2.0.dev2602231150315077",
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
"data_output_subpath": "output",
"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) (°) |
| 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. |
| 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 -->
@@ -28,7 +28,7 @@
```json
{
"general": {
"version": "0.2.0.dev58204789",
"version": "0.2.0.dev2602231150315077",
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
"data_output_subpath": "output",
"latitude": 52.52,
@@ -46,7 +46,7 @@
```json
{
"general": {
"version": "0.2.0.dev58204789",
"version": "0.2.0.dev2602231150315077",
"data_folder_path": "/home/user/.local/share/net.akkudoktoreos.net",
"data_output_subpath": "output",
"latitude": 52.52,

View File

@@ -1,6 +1,6 @@
# Akkudoktor-EOS
**Version**: `v0.2.0.dev58204789`
**Version**: `v0.2.0.dev2602231150315077`
<!-- 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.

View File

@@ -27,13 +27,13 @@ Example:
.. 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
.. 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
python -m venv .venv
.venv\Scripts\pip install --upgrade pip
.venv\Scripts\pip install -r requirements-dev.txt
.venv\Scripts\pip install build
.venv\Scripts\pip install -e .
uv run python scripts/get_version.py > version.txt
uv sync --extra dev
.. tab:: Linux
.. code-block:: bash
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r requirements-dev.txt
.venv/bin/pip install build
.venv/bin/pip install -e .
uv run python scripts/get_version.py > version.txt
uv sync --extra dev
.. tab:: Linux Make
@@ -103,25 +97,7 @@ This is recommended for developers who want to modify the source code and test c
make install
```
### Step 2.2 Activate the Virtual Environment
```{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
### Step 2.2 - Install pre-commit
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
python -m akkudoktoreos.server.eos
uv run python -m akkudoktoreos.server.eos
.. tab:: Linux
.. code-block:: bash
python -m akkudoktoreos.server.eos
uv run python -m akkudoktoreos.server.eos
.. tab:: Linux Make
@@ -194,13 +170,13 @@ interfere with the EOS server trying to start EOSdash.
.. 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
.. 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
@@ -216,13 +192,13 @@ interfere with the EOS server trying to start EOSdash.
.. 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
.. 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
@@ -381,13 +357,13 @@ At a minimum, you should run the module tests:
.. code-block:: powershell
pytest -vs --cov src --cov-report term-missing
uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux
.. code-block:: bash
pytest -vs --cov src --cov-report term-missing
uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux Make
@@ -413,13 +389,13 @@ resources:
.. 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
.. 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
@@ -437,13 +413,13 @@ To do profiling use:
.. code-block:: powershell
python tests/single_test_optimization.py --profile
uv run python tests/single_test_optimization.py --profile
.. tab:: Linux
.. code-block:: bash
python tests/single_test_optimization.py --profile
uv run python tests/single_test_optimization.py --profile
.. tab:: Linux Make
@@ -575,13 +551,13 @@ Ensure your changes do not break existing functionality:
.. code-block:: powershell
pytest -vs --cov src --cov-report term-missing
uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux
.. code-block:: bash
pytest -vs --cov src --cov-report term-missing
uv run pytest -vs --cov src --cov-report term-missing
.. tab:: Linux Make

View File

@@ -25,8 +25,8 @@ Before installing, ensure you have the following:
### For Source / Release Installation (M1/M2)
- Python 3.10 or higher
- pip
- Python 3.11+
- uv (recommended)
- Git (only for source)
- 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.
:::
## 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)
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
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt
.venv\Scripts\pip install -e .
uv sync --extra dev
.. tab:: Linux
.. code-block:: bash
python -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e .
uv sync --extra dev
```
@@ -108,13 +122,13 @@ Recommended for developers or users wanting the latest updates.
.. code-block:: powershell
.venv\Scripts\python -m akkudoktoreos.server.eos
uv run python -m akkudoktoreos.server.eos
.. tab:: Linux
.. 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
.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
.. 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 -->

View File

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

View File

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

View File

@@ -13,18 +13,87 @@ classifiers = [
"Programming Language :: Python :: 3",
"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]
Homepage = "https://github.com/Akkudoktor-EOS/EOS"
Issues = "https://github.com/Akkudoktor-EOS/EOS/issues"
[build-system]
requires = ["setuptools>=61.0"]
requires = ["setuptools>=77", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
optional-dependencies = {dev = { file = ["requirements-dev.txt"] }}
# version.txt must be generated
version = { file = "version.txt" }
@@ -92,6 +161,7 @@ markers = [
]
[tool.mypy]
mypy_path= "src"
files = ["src", "tests"]
exclude = "class_soc_calc\\.py$"
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
def find_cz() -> str:
venv = os.getenv("VIRTUAL_ENV")
paths = [Path(venv)] if venv else []
paths.append(Path.cwd() / ".venv")
def find_cz() -> list[str]:
"""Return command to invoke Commitizen via virtualenv or globally."""
candidates = []
for base in paths:
cz = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
if cz.exists():
return str(cz)
return "cz"
# 1⃣ Currently active virtualenv
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():
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():

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.
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
@@ -10,19 +16,40 @@ import sys
from pathlib import Path
def find_cz() -> str:
"""Find Commitizen executable, preferring virtualenv."""
venv = os.getenv("VIRTUAL_ENV")
paths = []
if venv:
paths.append(Path(venv))
paths.append(Path.cwd() / ".venv")
def find_cz() -> list[str]:
"""Return command to invoke Commitizen via virtualenv or globally."""
candidates = []
for base in paths:
cz = base / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
if cz.exists():
return str(cz)
return "cz"
# 1⃣ Currently active virtualenv
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():
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():
@@ -47,7 +74,7 @@ def main():
print(f"🔍 Checking commit message using {cz}...")
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.")
return 0
except subprocess.CalledProcessError:

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

@@ -1,10 +1,11 @@
#!/usr/bin/env python3
"""Pre-push hook: Commitizen check for *new commits only*.
Cross-platform + virtualenv-aware:
Cross-platform + uv/.venv aware:
- Prefers activated virtual environment (VIRTUAL_ENV)
- Falls back to ./.venv if found
- Falls back to global cz otherwise
- Falls back to uv-managed .uv/venv
- Falls back to .venv
- Falls back to global cz
"""
import os
@@ -13,23 +14,40 @@ import sys
from pathlib import Path
def find_cz_executable() -> str:
"""Return path to Commitizen executable, preferring virtual environments."""
# 1⃣ Active virtual environment (if running inside one)
venv_env = os.getenv("VIRTUAL_ENV")
if venv_env:
cz_path = Path(venv_env) / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
if cz_path.exists():
return str(cz_path)
def find_cz() -> list[str]:
"""Return command to invoke Commitizen via virtualenv or globally."""
candidates = []
# 2️⃣ Local .venv in repo root
repo_venv = Path.cwd() / ".venv"
cz_path = repo_venv / ("Scripts" if os.name == "nt" else "bin") / ("cz.exe" if os.name == "nt" else "cz")
if cz_path.exists():
return str(cz_path)
# 1️⃣ Currently active virtualenv
venv = os.getenv("VIRTUAL_ENV")
if venv:
candidates.append(Path(venv))
# 3️⃣ Global fallback
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 get_merge_base() -> str | None:
@@ -48,17 +66,17 @@ def get_merge_base() -> str | None:
def main() -> int:
cz = find_cz_executable()
cz = find_cz()
base = get_merge_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
print(f"🔍 Using {cz} to check new commits from {base}..HEAD ...")
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.")
return 0
except subprocess.CalledProcessError as e:

View File

@@ -14,6 +14,11 @@ from loguru import logger
from pydantic.fields import ComputedFieldInfo, FieldInfo
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.core.coreabc import get_config, singletons_init
from akkudoktoreos.core.pydantic import PydanticBaseModel

View File

@@ -18,6 +18,12 @@ import argparse
import json
import os
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.server.eos import app

View File

@@ -1,15 +1,29 @@
#!.venv/bin/python
"""Get version of EOS"""
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
# 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
SRC_DIR = PROJECT_ROOT / "src"
sys.path.insert(0, str(SRC_DIR))
from akkudoktoreos.core.version import __version__
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 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 ---
VERSION_PATTERNS = [
# 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
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]):
if not version:
raise ValueError("No version provided")
@@ -103,6 +124,8 @@ def main(version: str, files: List[str]):
if update_version_in_file(path, version):
updated_files.append(str(path))
updated_files.append(update_version_date_file())
if updated_files:
print(f"Updated files: {', '.join(updated_files)}")
else:

View File

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

View File

@@ -1,8 +1,21 @@
"""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 re
import subprocess
from dataclasses import dataclass
from datetime import ( # Don't use akkudoktoreos.utils.datetimeutil (-> pendulum)
datetime,
timezone,
)
from fnmatch import fnmatch
from pathlib import Path
from typing import Optional
@@ -14,14 +27,18 @@ VERSION_BASE = "0.2.0.dev"
# Project hash of relevant files
HASH_EOS = ""
# Number of digits to append to .dev to identify a development version
VERSION_DEV_PRECISION = 8
# Number of hash digits to append to .dev to identify a development version
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
DIR_PACKAGE_ROOT = Path(__file__).resolve().parent.parent
ALLOWED_SUFFIXES: set[str] = {".py", ".md", ".json"}
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
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
# ---------------------
def _version_hash() -> str:
"""Calculate project hash.
def _version_date_hash() -> tuple[datetime, str]:
"""Calculate project date and hash.
Only package files in src/akkudoktoreos can be hashed to make it work also for packages.
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"):
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,
)
return hash_digest
date = newest_commit_or_dirty_datetime(hashed_files)
return date, hash_digest
def _version_calculate() -> str:
"""Calculate the full version string.
For release versions: "x.y.z"
For dev versions: "x.y.z.dev<hash>"
For dev versions: "x.y.z.dev<date><hash>"
Returns:
Full version string
"""
if VERSION_BASE.endswith(".dev"):
# After dev only digits are allowed - convert hexdigest to digits
hash_value = int(_version_hash(), 16)
hash_digits = str(hash_value % (10**VERSION_DEV_PRECISION)).zfill(VERSION_DEV_PRECISION)
return f"{VERSION_BASE}{hash_digits}"
version_date, version_hash = _version_date_hash()
hash_value = int(version_hash, 16)
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:
# Release version - use base as-is
return VERSION_BASE
@@ -235,9 +318,9 @@ __version__ = _version_calculate()
VERSION_RE = re.compile(
r"""
^(?P<base>\d+\.\d+\.\d+) # x.y.z
(?:\. # .dev<hash> starts here
(?P<dev>dev) # literal 'dev'
(?P<hash>[a-f0-9]+)? # optional <hash> (hex digits)
(?:\.dev # literal '.dev' for development versions
(?P<date>\d{8}) # 8-digit date: YYMMDDHH
(?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:
x.y.z
x.y.z.dev
x.y.z.dev<HASH>
x.y.z.dev<date><hash>
Returns:
.. code-block:: python

View File

@@ -24,7 +24,7 @@ from xprocess import ProcessStarter, XProcess
from akkudoktoreos.config.config import ConfigEOS
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
# -----------------------------------------------
@@ -510,11 +510,14 @@ def server_base(
if extra_env:
env.update(extra_env)
# assure server to be installed
try:
project_dir = Path(__file__).parent.parent
@staticmethod
def _ensure_package(env: dict, project_dir: Path) -> None:
"""Ensure 'akkudoktoreos' is importable in this Python environment."""
try:
subprocess.run(
[sys.executable, "-c", "import", "akkudoktoreos.server.eos"],
[sys.executable, "-c", "import akkudoktoreos.server.eos"],
check=True,
env=env,
stdout=subprocess.PIPE,
@@ -522,14 +525,26 @@ def server_base(
cwd=project_dir,
)
except subprocess.CalledProcessError:
# If inside a normal venv or uv-managed environment, install in place
uv_root = os.getenv("UV_VENV_ROOT") # set by uv if active
venv_active = hasattr(sys, "real_prefix") or sys.prefix != sys.base_prefix
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)],
env=env,
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
args = [
@@ -644,7 +659,7 @@ def version_and_hash() -> Generator[dict[str, Optional[str]], None, None]:
Runs once per test session.
"""
info = version()
info["hash_current"] = _version_hash()
_, info["hash_current"] = _version_date_hash()
yield info

View File

@@ -14,10 +14,11 @@ from akkudoktoreos.core.version import (
EXCLUDED_FILES,
HashConfig,
_version_calculate,
_version_hash,
_version_date_hash,
collect_files,
hash_files,
)
from akkudoktoreos.utils.datetimeutil import to_datetime
DIR_PROJECT_ROOT = Path(__file__).parent.parent
GET_VERSION_SCRIPT = DIR_PROJECT_ROOT / "scripts" / "get_version.py"
@@ -127,7 +128,7 @@ def write_file(path: Path, content: str):
# -- Test version calculation ---
def test_version_hash() -> None:
def test_version_date_hash() -> None:
"""Test which files are used for version hash calculation."""
watched_paths = [DIR_PACKAGE_ROOT]
@@ -196,22 +197,26 @@ def test_version_non_dev(monkeypatch):
def test_version_dev_precision_8(monkeypatch):
"""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_DEV_PRECISION", 8)
monkeypatch.setattr("akkudoktoreos.core.version.VERSION_DEV_HASH_PRECISION", 8)
result = _version_calculate()
# Compute expected suffix using the same logic as _version_calculate
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 len(expected_digits) == 8
assert len(expected_hash_digits) == 8
assert result.startswith("0.2.0.dev")
assert result == expected
@@ -219,21 +224,25 @@ def test_version_dev_precision_8(monkeypatch):
def test_version_dev_precision_8_different_hash(monkeypatch):
"""A different hash must produce a different 8-digit suffix."""
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_DEV_PRECISION", 8)
monkeypatch.setattr("akkudoktoreos.core.version.VERSION_DEV_HASH_PRECISION", 8)
result = _version_calculate()
# Compute expected suffix using the same logic as _version_calculate
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 len(expected_digits) == 8
assert len(expected_hash_digits) == 8
assert result.startswith("0.2.0.dev")
assert result == expected

3427
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff