frequenz-cs-reporting 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- frequenz_cs_reporting-0.0.1/.streamlit/config.toml +10 -0
- frequenz_cs_reporting-0.0.1/LICENSE +21 -0
- frequenz_cs_reporting-0.0.1/MANIFEST.in +13 -0
- frequenz_cs_reporting-0.0.1/PKG-INFO +101 -0
- frequenz_cs_reporting-0.0.1/README.md +24 -0
- frequenz_cs_reporting-0.0.1/RELEASE_NOTES.md +16 -0
- frequenz_cs_reporting-0.0.1/app.py +211 -0
- frequenz_cs_reporting-0.0.1/pyproject.toml +221 -0
- frequenz_cs_reporting-0.0.1/setup.cfg +4 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/__init__.py +4 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/app_pages/__init__.py +4 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/app_pages/dashboard.py +159 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/app_pages/home.py +97 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/app_pages/solar.py +38 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/assets/__init__.py +4 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/assets/neustrom_background.png +0 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/assets/neustrom_logo.png +0 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/components/__init__.py +4 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/components/inputs.py +48 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/components/plot_charts.py +133 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/components/sidebar_inputs.py +164 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/components/tables.py +108 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/components/ui.py +67 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/conftest.py +13 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/constants.py +103 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/py.typed +0 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/rep_cs_core/__init__.py +4 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/rep_cs_core/config.py +39 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/rep_cs_core/page_spec.py +27 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/services/__init__.py +10 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/services/client_factory.py +97 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/services/data_service.py +100 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/utils/__init__.py +9 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/utils/env.py +29 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/utils/time.py +58 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/views/__init__.py +4 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/views/dashboard.py +173 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/views/metric_renderers.py +206 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/views/plot_renderers.py +287 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/views/sections.py +50 -0
- frequenz_cs_reporting-0.0.1/src/frequenz/frequenz_cs_reporting/views/table_renderers.py +204 -0
- frequenz_cs_reporting-0.0.1/src/frequenz_cs_reporting.egg-info/PKG-INFO +101 -0
- frequenz_cs_reporting-0.0.1/src/frequenz_cs_reporting.egg-info/SOURCES.txt +45 -0
- frequenz_cs_reporting-0.0.1/src/frequenz_cs_reporting.egg-info/dependency_links.txt +1 -0
- frequenz_cs_reporting-0.0.1/src/frequenz_cs_reporting.egg-info/requires.txt +61 -0
- frequenz_cs_reporting-0.0.1/src/frequenz_cs_reporting.egg-info/top_level.txt +1 -0
- frequenz_cs_reporting-0.0.1/toml_directory/__init__.py +5 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Frequenz Energy-as-a-Service GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
exclude .cookiecutter-replay.json
|
|
2
|
+
exclude .editorconfig
|
|
3
|
+
exclude .gitignore
|
|
4
|
+
exclude CODEOWNERS
|
|
5
|
+
exclude CONTRIBUTING.md
|
|
6
|
+
exclude mkdocs.yml
|
|
7
|
+
exclude noxfile.py
|
|
8
|
+
exclude src/conftest.py
|
|
9
|
+
recursive-exclude .github *
|
|
10
|
+
recursive-exclude benchmarks *
|
|
11
|
+
recursive-exclude docs *
|
|
12
|
+
recursive-exclude tests *
|
|
13
|
+
recursive-include py *.pyi
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: frequenz-cs-reporting
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Streamlit application for reporting and solar monitoring
|
|
5
|
+
Author-email: Frequenz Energy-as-a-Service GmbH <floss@frequenz.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Documentation, https://frequenz-floss.github.io/frequenz-cs-reporting/
|
|
8
|
+
Project-URL: Changelog, https://github.com/frequenz-floss/frequenz-cs-reporting/releases
|
|
9
|
+
Project-URL: Issues, https://github.com/frequenz-floss/frequenz-cs-reporting/issues
|
|
10
|
+
Project-URL: Repository, https://github.com/frequenz-floss/frequenz-cs-reporting
|
|
11
|
+
Project-URL: Support, https://github.com/frequenz-floss/frequenz-cs-reporting/discussions/categories/support
|
|
12
|
+
Keywords: frequenz,python,lib,library,frequenz-cs-reporting,tooling,notebooks,streamlit
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: <4,>=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: css-inline>=0.17.0
|
|
24
|
+
Requires-Dist: frequenz-lib-notebooks<0.15.0,>=0.13.0
|
|
25
|
+
Requires-Dist: matplotlib>=3.10.6
|
|
26
|
+
Requires-Dist: numpy>=2.3.2
|
|
27
|
+
Requires-Dist: pandas>=2.3.2
|
|
28
|
+
Requires-Dist: plotly>=6.3.0
|
|
29
|
+
Requires-Dist: pyarrow>=21.0.0
|
|
30
|
+
Requires-Dist: python-box>=7.3.2
|
|
31
|
+
Requires-Dist: python-dateutil>=2.9.0.post0
|
|
32
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
33
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
34
|
+
Requires-Dist: streamlit>=1.49.1
|
|
35
|
+
Requires-Dist: streamlit-aggrid>=1.1.8.post1
|
|
36
|
+
Requires-Dist: watchdog>=6.0.0
|
|
37
|
+
Provides-Extra: dev-flake8
|
|
38
|
+
Requires-Dist: flake8==7.3.0; extra == "dev-flake8"
|
|
39
|
+
Requires-Dist: flake8-docstrings==1.7.0; extra == "dev-flake8"
|
|
40
|
+
Requires-Dist: flake8-pyproject==1.2.4; extra == "dev-flake8"
|
|
41
|
+
Requires-Dist: pydoclint==0.8.3; extra == "dev-flake8"
|
|
42
|
+
Requires-Dist: pydocstyle==6.3.0; extra == "dev-flake8"
|
|
43
|
+
Provides-Extra: dev-formatting
|
|
44
|
+
Requires-Dist: black==26.1.0; extra == "dev-formatting"
|
|
45
|
+
Requires-Dist: isort==7.0.0; extra == "dev-formatting"
|
|
46
|
+
Provides-Extra: dev-mkdocs
|
|
47
|
+
Requires-Dist: Markdown==3.10; extra == "dev-mkdocs"
|
|
48
|
+
Requires-Dist: black==26.1.0; extra == "dev-mkdocs"
|
|
49
|
+
Requires-Dist: mike==2.1.3; extra == "dev-mkdocs"
|
|
50
|
+
Requires-Dist: mkdocs-gen-files==0.6.0; extra == "dev-mkdocs"
|
|
51
|
+
Requires-Dist: mkdocs-literate-nav==0.6.2; extra == "dev-mkdocs"
|
|
52
|
+
Requires-Dist: mkdocs-macros-plugin==1.5.0; extra == "dev-mkdocs"
|
|
53
|
+
Requires-Dist: mkdocs-material==9.7.1; extra == "dev-mkdocs"
|
|
54
|
+
Requires-Dist: mkdocstrings[python]==1.0.1; extra == "dev-mkdocs"
|
|
55
|
+
Requires-Dist: mkdocstrings-python==2.0.1; extra == "dev-mkdocs"
|
|
56
|
+
Requires-Dist: frequenz-repo-config[lib]==0.14.0; extra == "dev-mkdocs"
|
|
57
|
+
Provides-Extra: dev-mypy
|
|
58
|
+
Requires-Dist: mypy==1.19.1; extra == "dev-mypy"
|
|
59
|
+
Requires-Dist: types-Markdown==3.10.0.20251106; extra == "dev-mypy"
|
|
60
|
+
Requires-Dist: pandas-stubs; extra == "dev-mypy"
|
|
61
|
+
Requires-Dist: frequenz-cs-reporting[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-mypy"
|
|
62
|
+
Provides-Extra: dev-noxfile
|
|
63
|
+
Requires-Dist: nox==2025.11.12; extra == "dev-noxfile"
|
|
64
|
+
Requires-Dist: frequenz-repo-config[lib]==0.14.0; extra == "dev-noxfile"
|
|
65
|
+
Provides-Extra: dev-pylint
|
|
66
|
+
Requires-Dist: frequenz-cs-reporting[dev-mkdocs,dev-noxfile,dev-pytest]; extra == "dev-pylint"
|
|
67
|
+
Provides-Extra: dev-pytest
|
|
68
|
+
Requires-Dist: pytest==9.0.2; extra == "dev-pytest"
|
|
69
|
+
Requires-Dist: pylint==4.0.4; extra == "dev-pytest"
|
|
70
|
+
Requires-Dist: frequenz-repo-config[extra-lint-examples]==0.14.0; extra == "dev-pytest"
|
|
71
|
+
Requires-Dist: pytest-mock==3.15.1; extra == "dev-pytest"
|
|
72
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "dev-pytest"
|
|
73
|
+
Requires-Dist: async-solipsism==0.9; extra == "dev-pytest"
|
|
74
|
+
Provides-Extra: dev
|
|
75
|
+
Requires-Dist: frequenz-cs-reporting[dev-flake8,dev-formatting,dev-mkdocs,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]; extra == "dev"
|
|
76
|
+
Dynamic: license-file
|
|
77
|
+
|
|
78
|
+
# Frequenz CS Reporting Library
|
|
79
|
+
|
|
80
|
+
[](https://github.com/frequenz-floss/frequenz-cs-reporting/actions/workflows/ci.yaml)
|
|
81
|
+
[](https://pypi.org/project/frequenz-cs-reporting/)
|
|
82
|
+
[](https://frequenz-floss.github.io/frequenz-cs-reporting/)
|
|
83
|
+
|
|
84
|
+
## Introduction
|
|
85
|
+
|
|
86
|
+
Streamlit application for reporting and solar monitoring
|
|
87
|
+
|
|
88
|
+
TODO(cookiecutter): Improve the README file
|
|
89
|
+
|
|
90
|
+
## Supported Platforms
|
|
91
|
+
|
|
92
|
+
The following platforms are officially supported (tested):
|
|
93
|
+
|
|
94
|
+
- **Python:** 3.11
|
|
95
|
+
- **Operating System:** Ubuntu Linux 20.04
|
|
96
|
+
- **Architectures:** amd64, arm64
|
|
97
|
+
|
|
98
|
+
## Contributing
|
|
99
|
+
|
|
100
|
+
If you want to know how to build this project and contribute to it, please
|
|
101
|
+
check out the [Contributing Guide](CONTRIBUTING.md).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Frequenz CS Reporting Library
|
|
2
|
+
|
|
3
|
+
[](https://github.com/frequenz-floss/frequenz-cs-reporting/actions/workflows/ci.yaml)
|
|
4
|
+
[](https://pypi.org/project/frequenz-cs-reporting/)
|
|
5
|
+
[](https://frequenz-floss.github.io/frequenz-cs-reporting/)
|
|
6
|
+
|
|
7
|
+
## Introduction
|
|
8
|
+
|
|
9
|
+
Streamlit application for reporting and solar monitoring
|
|
10
|
+
|
|
11
|
+
TODO(cookiecutter): Improve the README file
|
|
12
|
+
|
|
13
|
+
## Supported Platforms
|
|
14
|
+
|
|
15
|
+
The following platforms are officially supported (tested):
|
|
16
|
+
|
|
17
|
+
- **Python:** 3.11
|
|
18
|
+
- **Operating System:** Ubuntu Linux 20.04
|
|
19
|
+
- **Architectures:** amd64, arm64
|
|
20
|
+
|
|
21
|
+
## Contributing
|
|
22
|
+
|
|
23
|
+
If you want to know how to build this project and contribute to it, please
|
|
24
|
+
check out the [Contributing Guide](CONTRIBUTING.md).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Frequenz CS Reporting Library Release Notes
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
- Initial Streamlit-based reporting app that discovers packaged pages, shows branded navigation, and renders microgrid reporting dashboards backed by the frequenz data stack.
|
|
5
|
+
|
|
6
|
+
## Upgrading
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## New Features
|
|
10
|
+
- Added app.py entrypoint that loads PageSpec definitions, persists navigation in query params, and renders sidebar branding.
|
|
11
|
+
- New Home and Reporting pages: the dashboard offers microgrid/date/timezone/resolution filters, fetches/caches microgrid power data, builds a master dataframe, and drives overview metrics plus consumption breakdowns.
|
|
12
|
+
- Dashboard visuals now include time-series plots, energy-mix pie, component-specific tabs (PV, battery, wind, BHKW, EV), and styled plot cards.
|
|
13
|
+
- Data tables use AgGrid with CSV downloads for power mix, component analyses, and the combined master dataframe; reusable UI helpers and constants were added for consistent styling and column naming.
|
|
14
|
+
|
|
15
|
+
## Bug Fixes
|
|
16
|
+
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# License: MIT
|
|
2
|
+
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH
|
|
3
|
+
|
|
4
|
+
"""Main application module for the Frequenz CS Reporting Suite."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import importlib.resources as resources
|
|
12
|
+
import importlib.util
|
|
13
|
+
import streamlit as st
|
|
14
|
+
import pkgutil
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
# --- Check if running in deepnote to pick up environment variables ---
|
|
18
|
+
def running_in_deepnote() -> bool:
|
|
19
|
+
# Common Deepnote env vars (if one changes, the others may still work)
|
|
20
|
+
return any(k in os.environ for k in (
|
|
21
|
+
"DEEPNOTE_PROJECT_ID",
|
|
22
|
+
"DEEPNOTE_WORKSPACE_ID",
|
|
23
|
+
"DEEPNOTE",
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
IN_DEEPNOTE = running_in_deepnote()
|
|
27
|
+
|
|
28
|
+
if IN_DEEPNOTE:
|
|
29
|
+
import deepnote_toolkit
|
|
30
|
+
deepnote_toolkit.set_integration_env()
|
|
31
|
+
|
|
32
|
+
# --- Project paths & sys.path wiring ----------------------------------------
|
|
33
|
+
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
34
|
+
LIB_PAGES_ROOT = "frequenz.frequenz_cs_reporting.app_pages"
|
|
35
|
+
LOGO_NAME = "neustrom_logo.png"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _resolve_package_root() -> Path:
|
|
39
|
+
"""Return the installed frequenz package path, with a local fallback for dev."""
|
|
40
|
+
spec = importlib.util.find_spec("frequenz.frequenz_cs_reporting")
|
|
41
|
+
if spec and spec.submodule_search_locations:
|
|
42
|
+
return Path(next(iter(spec.submodule_search_locations))).resolve()
|
|
43
|
+
if spec and spec.origin:
|
|
44
|
+
return Path(spec.origin).resolve().parent
|
|
45
|
+
return (PACKAGE_DIR / "src" / "frequenz" / "frequenz_cs_reporting").resolve()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
PACKAGE_ROOT = _resolve_package_root()
|
|
49
|
+
APP_PAGES_DIR = PACKAGE_ROOT / "app_pages"
|
|
50
|
+
ASSETS_DIR = PACKAGE_ROOT / "assets"
|
|
51
|
+
|
|
52
|
+
package_parent = str(PACKAGE_ROOT.parent)
|
|
53
|
+
if package_parent not in sys.path:
|
|
54
|
+
sys.path.insert(0, package_parent)
|
|
55
|
+
|
|
56
|
+
# Import PageSpec from your custom library
|
|
57
|
+
from frequenz.frequenz_cs_reporting.rep_cs_core.page_spec import PageSpec
|
|
58
|
+
|
|
59
|
+
# --- Local page loader (from ./app_pages) -----------------------------------
|
|
60
|
+
def _load_local_pages() -> list[PageSpec]:
|
|
61
|
+
"""Load PAGE specs from local app_pages/*.py files."""
|
|
62
|
+
if not APP_PAGES_DIR.exists():
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
pages: list[PageSpec] = []
|
|
66
|
+
|
|
67
|
+
for module_path in sorted(APP_PAGES_DIR.glob("*.py")):
|
|
68
|
+
if module_path.name.startswith("_") or module_path.name == "__init__.py":
|
|
69
|
+
continue
|
|
70
|
+
spec = importlib.util.spec_from_file_location(module_path.stem, module_path)
|
|
71
|
+
if spec is None or spec.loader is None:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
module = importlib.util.module_from_spec(spec)
|
|
75
|
+
spec.loader.exec_module(module)
|
|
76
|
+
page = getattr(module, "PAGE", None)
|
|
77
|
+
|
|
78
|
+
if page and all(
|
|
79
|
+
hasattr(page, attr)
|
|
80
|
+
for attr in ("key", "title", "icon", "order", "render")
|
|
81
|
+
):
|
|
82
|
+
pages.append(page)
|
|
83
|
+
|
|
84
|
+
return pages
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- Library page discovery (from frequenz.app_pages) -----------------------
|
|
88
|
+
def discover_library_pages(pkg_root: str = LIB_PAGES_ROOT) -> list[PageSpec]:
|
|
89
|
+
"""Discover PAGE specs from the installed frequenz app_pages package.
|
|
90
|
+
|
|
91
|
+
Falls back to local ./app_pages if the library package is missing.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
pkg = importlib.import_module(pkg_root)
|
|
95
|
+
except ModuleNotFoundError:
|
|
96
|
+
# Library not installed; fall back to local pages
|
|
97
|
+
return _load_local_pages()
|
|
98
|
+
|
|
99
|
+
pages: list[PageSpec] = []
|
|
100
|
+
|
|
101
|
+
for _, modname, _ in pkgutil.iter_modules(pkg.__path__, pkg.__name__ + "."):
|
|
102
|
+
short = modname.rsplit(".", 1)[-1]
|
|
103
|
+
if short.startswith("_"):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
module = importlib.import_module(modname)
|
|
107
|
+
page = getattr(module, "PAGE", None)
|
|
108
|
+
|
|
109
|
+
if page and all(
|
|
110
|
+
hasattr(page, attr)
|
|
111
|
+
for attr in ("key", "title", "icon", "order", "render")
|
|
112
|
+
):
|
|
113
|
+
pages.append(page)
|
|
114
|
+
|
|
115
|
+
# Sort by (order, title) for stable navigation
|
|
116
|
+
return sorted(pages, key=lambda p: (p.order, p.title.lower()))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _load_logo_bytes() -> bytes | None:
|
|
120
|
+
"""Fetch the sidebar logo from the installed package resources."""
|
|
121
|
+
try:
|
|
122
|
+
return resources.files("frequenz.frequenz_cs_reporting.assets").joinpath(LOGO_NAME).read_bytes()
|
|
123
|
+
except (FileNotFoundError, ModuleNotFoundError):
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
local_logo = ASSETS_DIR / LOGO_NAME
|
|
127
|
+
if local_logo.exists():
|
|
128
|
+
return local_logo.read_bytes()
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Sidebar navigation
|
|
133
|
+
def sidebar(pages: list[PageSpec]) -> PageSpec:
|
|
134
|
+
if logo_bytes := _load_logo_bytes():
|
|
135
|
+
st.sidebar.image(logo_bytes, width='stretch')
|
|
136
|
+
|
|
137
|
+
st.sidebar.divider()
|
|
138
|
+
|
|
139
|
+
state_key = "selected_page"
|
|
140
|
+
valid_keys = {p.key for p in pages}
|
|
141
|
+
default_key = st.query_params.get("page", [pages[0].key])[0]
|
|
142
|
+
|
|
143
|
+
if default_key not in valid_keys:
|
|
144
|
+
default_key = pages[0].key
|
|
145
|
+
|
|
146
|
+
# Initialize session state with the URL query parameter
|
|
147
|
+
st.session_state.setdefault(state_key, default_key)
|
|
148
|
+
|
|
149
|
+
# Ensure the state matches the URL param on the initial load/rerun
|
|
150
|
+
if st.session_state[state_key] != default_key:
|
|
151
|
+
st.session_state[state_key] = default_key
|
|
152
|
+
|
|
153
|
+
# Map display labels to internal keys
|
|
154
|
+
options = {f"{p.icon} {p.title}": p.key for p in pages}
|
|
155
|
+
display_options = list(options.keys())
|
|
156
|
+
|
|
157
|
+
current_key = st.session_state[state_key]
|
|
158
|
+
initial_index = next(
|
|
159
|
+
(idx for idx, page in enumerate(pages) if page.key == current_key), 0
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Function to resolve the selected key from the display label
|
|
163
|
+
def get_key_from_label(label: str) -> str:
|
|
164
|
+
return options.get(label, pages[0].key)
|
|
165
|
+
|
|
166
|
+
# Add page navigation header
|
|
167
|
+
st.sidebar.header("📑 Seiten")
|
|
168
|
+
|
|
169
|
+
# Use st.sidebar.radio to manage selection
|
|
170
|
+
selected_label = st.sidebar.radio(
|
|
171
|
+
"Navigation",
|
|
172
|
+
options=display_options,
|
|
173
|
+
index=initial_index,
|
|
174
|
+
key="navigation_radio",
|
|
175
|
+
label_visibility="collapsed",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Add divider after navigation
|
|
179
|
+
st.sidebar.divider()
|
|
180
|
+
|
|
181
|
+
# Update session state and query params based on radio selection
|
|
182
|
+
selected_key = get_key_from_label(selected_label)
|
|
183
|
+
if st.session_state[state_key] != selected_key:
|
|
184
|
+
st.session_state[state_key] = selected_key
|
|
185
|
+
st.query_params.page = selected_key
|
|
186
|
+
|
|
187
|
+
selected = next(p for p in pages if p.key == st.session_state[state_key])
|
|
188
|
+
return selected
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# --- Main entrypoint --------------------------------------------------------
|
|
192
|
+
def main() -> None:
|
|
193
|
+
st.set_page_config(
|
|
194
|
+
page_title="Enterprise Reporting App",
|
|
195
|
+
page_icon="🏢",
|
|
196
|
+
layout="wide",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
pages = discover_library_pages()
|
|
200
|
+
if not pages:
|
|
201
|
+
st.info(f"No pages found under `{LIB_PAGES_ROOT}` (or local app_pages).")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
selected = sidebar(pages)
|
|
205
|
+
|
|
206
|
+
# Render selected page
|
|
207
|
+
selected.render()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
main()
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# License: MIT
|
|
2
|
+
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH
|
|
3
|
+
|
|
4
|
+
[build-system]
|
|
5
|
+
requires = [
|
|
6
|
+
"setuptools == 80.9.0",
|
|
7
|
+
"setuptools_scm[toml] == 9.2.2",
|
|
8
|
+
"frequenz-repo-config[lib] == 0.14.0",
|
|
9
|
+
]
|
|
10
|
+
build-backend = "setuptools.build_meta"
|
|
11
|
+
|
|
12
|
+
[project]
|
|
13
|
+
name = "frequenz-cs-reporting"
|
|
14
|
+
description = "Streamlit application for reporting and solar monitoring"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
dynamic = ["version"]
|
|
17
|
+
license = { text = "MIT" }
|
|
18
|
+
keywords = ["frequenz", "python", "lib", "library", "frequenz-cs-reporting", "tooling", "notebooks", "streamlit"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">= 3.11, < 4"
|
|
29
|
+
|
|
30
|
+
dependencies = [
|
|
31
|
+
"css-inline>=0.17.0",
|
|
32
|
+
"frequenz-lib-notebooks>=0.13.0, < 0.15.0",
|
|
33
|
+
"matplotlib>=3.10.6",
|
|
34
|
+
"numpy>=2.3.2",
|
|
35
|
+
"pandas>=2.3.2",
|
|
36
|
+
"plotly>=6.3.0",
|
|
37
|
+
"pyarrow>=21.0.0",
|
|
38
|
+
"python-box>=7.3.2",
|
|
39
|
+
"python-dateutil>=2.9.0.post0",
|
|
40
|
+
"python-dotenv>=1.1.1",
|
|
41
|
+
"pyyaml>=6.0.3",
|
|
42
|
+
"streamlit>=1.49.1",
|
|
43
|
+
"streamlit-aggrid>=1.1.8.post1",
|
|
44
|
+
"watchdog>=6.0.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[[project.authors]]
|
|
48
|
+
name = "Frequenz Energy-as-a-Service GmbH"
|
|
49
|
+
email = "floss@frequenz.com"
|
|
50
|
+
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
dev-flake8 = [
|
|
53
|
+
"flake8 == 7.3.0",
|
|
54
|
+
"flake8-docstrings == 1.7.0",
|
|
55
|
+
"flake8-pyproject == 1.2.4", # For reading the flake8 config from pyproject.toml
|
|
56
|
+
"pydoclint == 0.8.3",
|
|
57
|
+
"pydocstyle == 6.3.0",
|
|
58
|
+
]
|
|
59
|
+
dev-formatting = ["black == 26.1.0", "isort == 7.0.0"]
|
|
60
|
+
dev-mkdocs = [
|
|
61
|
+
"Markdown == 3.10",
|
|
62
|
+
"black == 26.1.0",
|
|
63
|
+
"mike == 2.1.3",
|
|
64
|
+
"mkdocs-gen-files == 0.6.0",
|
|
65
|
+
"mkdocs-literate-nav == 0.6.2",
|
|
66
|
+
"mkdocs-macros-plugin == 1.5.0",
|
|
67
|
+
"mkdocs-material == 9.7.1",
|
|
68
|
+
"mkdocstrings[python] == 1.0.1",
|
|
69
|
+
"mkdocstrings-python == 2.0.1",
|
|
70
|
+
"frequenz-repo-config[lib] == 0.14.0",
|
|
71
|
+
]
|
|
72
|
+
dev-mypy = [
|
|
73
|
+
"mypy == 1.19.1",
|
|
74
|
+
"types-Markdown == 3.10.0.20251106",
|
|
75
|
+
"pandas-stubs",
|
|
76
|
+
# For checking the noxfile, docs/ script, and tests
|
|
77
|
+
"frequenz-cs-reporting[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
78
|
+
]
|
|
79
|
+
dev-noxfile = [
|
|
80
|
+
"nox == 2025.11.12",
|
|
81
|
+
"frequenz-repo-config[lib] == 0.14.0",
|
|
82
|
+
]
|
|
83
|
+
dev-pylint = [
|
|
84
|
+
# dev-pytest already defines a dependency to pylint because of the examples
|
|
85
|
+
# For checking the noxfile, docs/ script, and tests
|
|
86
|
+
"frequenz-cs-reporting[dev-mkdocs,dev-noxfile,dev-pytest]",
|
|
87
|
+
]
|
|
88
|
+
dev-pytest = [
|
|
89
|
+
"pytest == 9.0.2",
|
|
90
|
+
"pylint == 4.0.4", # We need this to check for the examples
|
|
91
|
+
"frequenz-repo-config[extra-lint-examples] == 0.14.0",
|
|
92
|
+
"pytest-mock == 3.15.1",
|
|
93
|
+
"pytest-asyncio == 1.3.0",
|
|
94
|
+
"async-solipsism == 0.9",
|
|
95
|
+
]
|
|
96
|
+
dev = [
|
|
97
|
+
"frequenz-cs-reporting[dev-mkdocs,dev-flake8,dev-formatting,dev-mypy,dev-noxfile,dev-pylint,dev-pytest]",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
[project.urls]
|
|
101
|
+
Documentation = "https://frequenz-floss.github.io/frequenz-cs-reporting/"
|
|
102
|
+
Changelog = "https://github.com/frequenz-floss/frequenz-cs-reporting/releases"
|
|
103
|
+
Issues = "https://github.com/frequenz-floss/frequenz-cs-reporting/issues"
|
|
104
|
+
Repository = "https://github.com/frequenz-floss/frequenz-cs-reporting"
|
|
105
|
+
Support = "https://github.com/frequenz-floss/frequenz-cs-reporting/discussions/categories/support"
|
|
106
|
+
|
|
107
|
+
[tool.setuptools]
|
|
108
|
+
# This tells setuptools to look for packages inside the 'src' directory.
|
|
109
|
+
package-dir = {"" = "src"}
|
|
110
|
+
include-package-data = true
|
|
111
|
+
|
|
112
|
+
[tool.setuptools.packages.find]
|
|
113
|
+
where = ["src"]
|
|
114
|
+
include = ["frequenz*"]
|
|
115
|
+
namespaces = true
|
|
116
|
+
|
|
117
|
+
[tool.setuptools.package-data]
|
|
118
|
+
"frequenz.frequenz_cs_reporting" = [
|
|
119
|
+
"assets/*.png",
|
|
120
|
+
"assets/*.jpg",
|
|
121
|
+
"assets/*.svg",
|
|
122
|
+
"assets/*.json",
|
|
123
|
+
".streamlit/config.toml",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
[tool.black]
|
|
127
|
+
line-length = 88
|
|
128
|
+
target-version = ["py311", "py312"]
|
|
129
|
+
|
|
130
|
+
[tool.isort]
|
|
131
|
+
profile = "black"
|
|
132
|
+
line_length = 88
|
|
133
|
+
src_paths = ["benchmarks", "examples", "src", "tests"]
|
|
134
|
+
|
|
135
|
+
[tool.flake8]
|
|
136
|
+
# We give some flexibility to go over 88, there are cases like long URLs or
|
|
137
|
+
# code in documenation that have extra indentation. Black will still take care
|
|
138
|
+
# of making everything that can be 88 wide, 88 wide.
|
|
139
|
+
max-line-length = 100
|
|
140
|
+
extend-ignore = [
|
|
141
|
+
"E203", # Whitespace before ':' (conflicts with black)
|
|
142
|
+
"W503", # Line break before binary operator (conflicts with black)
|
|
143
|
+
]
|
|
144
|
+
# pydoclint options
|
|
145
|
+
style = "google"
|
|
146
|
+
check-return-types = false
|
|
147
|
+
check-yield-types = false
|
|
148
|
+
arg-type-hints-in-docstring = false
|
|
149
|
+
arg-type-hints-in-signature = true
|
|
150
|
+
allow-init-docstring = true
|
|
151
|
+
check-class-attributes = false
|
|
152
|
+
|
|
153
|
+
[tool.pylint.similarities]
|
|
154
|
+
ignore-comments = ['yes']
|
|
155
|
+
ignore-docstrings = ['yes']
|
|
156
|
+
ignore-imports = ['no']
|
|
157
|
+
min-similarity-lines = 40
|
|
158
|
+
|
|
159
|
+
[tool.pylint.messages_control]
|
|
160
|
+
disable = [
|
|
161
|
+
"too-few-public-methods",
|
|
162
|
+
"too-many-return-statements",
|
|
163
|
+
# disabled because it conflicts with isort
|
|
164
|
+
"wrong-import-order",
|
|
165
|
+
"ungrouped-imports",
|
|
166
|
+
# Checked by mypy (and pylint is very flaky checking these)
|
|
167
|
+
"unsubscriptable-object",
|
|
168
|
+
"no-member",
|
|
169
|
+
"no-name-in-module",
|
|
170
|
+
"possibly-used-before-assignment",
|
|
171
|
+
# Checked by flake8
|
|
172
|
+
"f-string-without-interpolation",
|
|
173
|
+
"line-too-long",
|
|
174
|
+
"missing-function-docstring",
|
|
175
|
+
"redefined-outer-name",
|
|
176
|
+
"unnecessary-lambda-assignment",
|
|
177
|
+
"unused-import",
|
|
178
|
+
"unused-variable",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
[tool.pytest.ini_options]
|
|
182
|
+
addopts = "-vv"
|
|
183
|
+
filterwarnings = [
|
|
184
|
+
"error",
|
|
185
|
+
"once::DeprecationWarning",
|
|
186
|
+
"once::PendingDeprecationWarning",
|
|
187
|
+
# We ignore warnings about protobuf gencode version being one version older
|
|
188
|
+
# than the current version, as this is supported by protobuf, and we expect to
|
|
189
|
+
# have such cases. If we go too far, we will get a proper error anyways.
|
|
190
|
+
# We use a raw string (single quotes) to avoid the need to escape special
|
|
191
|
+
# characters as this is a regex.
|
|
192
|
+
'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning',
|
|
193
|
+
]
|
|
194
|
+
testpaths = ["tests", "src"]
|
|
195
|
+
asyncio_mode = "auto"
|
|
196
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
197
|
+
required_plugins = ["pytest-asyncio", "pytest-mock"]
|
|
198
|
+
|
|
199
|
+
[tool.mypy]
|
|
200
|
+
explicit_package_bases = true
|
|
201
|
+
namespace_packages = true
|
|
202
|
+
# This option disables mypy cache, and it is sometimes useful to enable it if
|
|
203
|
+
# you are getting weird intermittent error, or error in the CI but not locally
|
|
204
|
+
# (or vice versa). In particular errors saying that type: ignore is not
|
|
205
|
+
# used but getting the original ignored error when removing the type: ignore.
|
|
206
|
+
# See for example: https://github.com/python/mypy/issues/2960
|
|
207
|
+
#no_incremental = true
|
|
208
|
+
packages = ["frequenz.frequenz_cs_reporting"]
|
|
209
|
+
strict = true
|
|
210
|
+
|
|
211
|
+
[[tool.mypy.overrides]]
|
|
212
|
+
module = ["mkdocs_macros.*", "sybil", "sybil.*", "pvlib", "pvlib.*", "plotly", "plotly.*"]
|
|
213
|
+
ignore_missing_imports = true
|
|
214
|
+
|
|
215
|
+
[tool.setuptools_scm]
|
|
216
|
+
version_scheme = "post-release"
|
|
217
|
+
|
|
218
|
+
[dependency-groups]
|
|
219
|
+
dev = [
|
|
220
|
+
"pandas-stubs>=2.3.3.260113",
|
|
221
|
+
]
|