xploreai 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.3
2
+ Name: xploreai
3
+ Version: 0.2.0
4
+ Summary: Library and cookbook codes for AI Engineering Projects
5
+ Author: Daniel Dominguete
6
+ Author-email: Daniel Dominguete <daniel.dominguete@gmail.com>
7
+ Requires-Dist: pytest>=9.2.0
8
+ Requires-Python: >=3.12, <3.13
9
+ Description-Content-Type: text/markdown
10
+
11
+ # XploreAI
12
+
13
+ Biblioteca Python e cookbook para experimentos em engenharia de AI
14
+
15
+ ## Estrutura do repositório
16
+
17
+ ```
18
+ src/xploreai/ → biblioteca (publicável no PyPI)
19
+ cookbook/ → scripts e notebooks de receitas locais (ex.: langchain/)
20
+ references/ → material de estudo externo (somente leitura)
21
+ ```
22
+
23
+ ## Módulos da biblioteca (`src/xploreai/`)
24
+
25
+ Pacotes por domínio; cada módulo segue o prefixo **`xai_`** (Xplore AI).
26
+
27
+ | Pacote | Arquivo | Responsabilidade |
28
+ |--------|---------|------------------|
29
+ | `environment` | `xai_localhost.py` | Estrutura de pastas para runs locais |
30
+ | `log` | `xai_log.py` | Logging estruturado e perfil de ambiente em execuções |
31
+ | `utils` | `xai_environment.py` | Localizar a raiz do projeto (`pyproject.toml`) |
32
+ | `utils` | `xai_memory.py` | Conversão e utilitários de memória (unidades binárias) |
33
+
34
+ ## Configuração do ambiente
35
+
36
+ Este projeto usa **[uv](https://docs.astral.sh/uv/)** para ambientes e dependências.
37
+ Não use `requirements.txt` na raiz nem `pip install` — tudo fica em **`pyproject.toml`** + **`uv.lock`**.
38
+
39
+ ### 1. Instalar uv
40
+
41
+ ```bash
42
+ curl -LsSf https://astral.sh/uv/install.sh | sh
43
+ ```
44
+
45
+ ### 2. Inicializar ambiente do projeto
46
+
47
+ Na raiz do repositório (onde está o `pyproject.toml`):
48
+
49
+ ```bash
50
+ uv sync --all-groups
51
+ ```
52
+
53
+ O `uv sync` cria `.venv/`, instala dependências fixadas em `uv.lock` e registra **`xploreai`** em modo editável a partir de `src/xploreai/`.
54
+
55
+ Para **criar um projeto Python novo** com uv (fora deste repo):
56
+
57
+ ```bash
58
+ uv init --python 3.12
59
+ uv add pandas # exemplo: adiciona dependencia e atualiza pyproject.toml + uv.lock
60
+ uv sync
61
+ ```
62
+
63
+ ### 3. Notebooks sem output no Git (nbstripout)
64
+
65
+ Este repo **nao usa** o pacote [`pre-commit`](https://pre-commit.com/) para notebooks. A limpeza e feita pelo **filtro Git** do [`nbstripout`](https://github.com/kynan/nbstripout) (ja dependencia em `pyproject.toml`).
66
+
67
+ **Uma vez por clone** (depois de `uv sync`), na raiz do repo:
68
+
69
+ ```bash
70
+ uv run nbstripout --install --attributes .gitattributes
71
+ ```
72
+
73
+ Isso registra o filtro em `.git/config` e versiona regras em `.gitattributes` (`*.ipynb filter=nbstripout`). No `git add`, outputs sao removidos do que entra no indice; no working tree voce pode continuar executando o notebook normalmente.
74
+
75
+ Conferir se esta ativo:
76
+
77
+ ```bash
78
+ uv run nbstripout --is-installed
79
+ uv run nbstripout --status
80
+ ```
81
+
82
+ Se recriar o `.venv` ou mudar o caminho do Python, reinstale o filtro:
83
+
84
+ ```bash
85
+ uv run nbstripout --install --attributes .gitattributes --force
86
+ ```
87
+
88
+ Limpar notebooks ja versionados (one-shot), se necessario:
89
+
90
+ ```bash
91
+ uv run nbstripout $(git ls-files '*.ipynb')
92
+ ```
93
+
94
+ Validar CI/local sem alterar arquivos:
95
+
96
+ ```bash
97
+ uv run nbstripout --verify $(git ls-files '*.ipynb')
98
+ ```
99
+
100
+ ### 4. Execução
101
+
102
+ Validação do setup:
103
+
104
+ ```bash
105
+ uv run xploreai
106
+ ```
107
+
108
+ Testes:
109
+
110
+ ```bash
111
+ uv run pytest
112
+ ```
113
+
114
+ Exemplo de cookbook:
115
+
116
+ ```bash
117
+ uv run python cookbook/01_langchain/prompt_template.py
118
+ ```
119
+
120
+ ## Versionamento semântico (Commitizen)
121
+
122
+ Commits seguem **[Conventional Commits](https://www.conventionalcommits.org/)** via [Commitizen](https://commitizen-tools.github.io/commitizen/). A versão do pacote vive em `pyproject.toml` (`[project].version`); o `cz bump` atualiza essa versão, o `CHANGELOG.md` e cria a tag Git `vX.Y.Z`.
123
+
124
+ ### Commits e push
125
+
126
+ ```bash
127
+ git add .
128
+ uv run cz commit
129
+ git push # sem publicação de nova versão
130
+ ```
131
+
132
+ | Tipo | Quando usar | Bump |
133
+ |------|-------------|------|
134
+ | `feat` | Nova funcionalidade | minor |
135
+ | `fix` | Correção de bug | patch |
136
+ | `docs` | Documentação | — |
137
+ | `refactor` | Refatoração | patch |
138
+ | `test` | Testes | — |
139
+ | `chore` | Manutenção | — |
140
+
141
+ ### Release e publicação
142
+
143
+ ```bash
144
+ uv run cz bump # bump de versão + CHANGELOG + tag
145
+ uv build # gera dist/*.whl
146
+ uv run --with xploreai --no-project -- python -c "import xploreai" # testar artefato
147
+ uv run --env-file .env --no-project -- uv publish # PyPI (token em UV_PUBLISH_TOKEN)
148
+ git push && git push --tags
149
+ ```
@@ -0,0 +1,139 @@
1
+ # XploreAI
2
+
3
+ Biblioteca Python e cookbook para experimentos em engenharia de AI
4
+
5
+ ## Estrutura do repositório
6
+
7
+ ```
8
+ src/xploreai/ → biblioteca (publicável no PyPI)
9
+ cookbook/ → scripts e notebooks de receitas locais (ex.: langchain/)
10
+ references/ → material de estudo externo (somente leitura)
11
+ ```
12
+
13
+ ## Módulos da biblioteca (`src/xploreai/`)
14
+
15
+ Pacotes por domínio; cada módulo segue o prefixo **`xai_`** (Xplore AI).
16
+
17
+ | Pacote | Arquivo | Responsabilidade |
18
+ |--------|---------|------------------|
19
+ | `environment` | `xai_localhost.py` | Estrutura de pastas para runs locais |
20
+ | `log` | `xai_log.py` | Logging estruturado e perfil de ambiente em execuções |
21
+ | `utils` | `xai_environment.py` | Localizar a raiz do projeto (`pyproject.toml`) |
22
+ | `utils` | `xai_memory.py` | Conversão e utilitários de memória (unidades binárias) |
23
+
24
+ ## Configuração do ambiente
25
+
26
+ Este projeto usa **[uv](https://docs.astral.sh/uv/)** para ambientes e dependências.
27
+ Não use `requirements.txt` na raiz nem `pip install` — tudo fica em **`pyproject.toml`** + **`uv.lock`**.
28
+
29
+ ### 1. Instalar uv
30
+
31
+ ```bash
32
+ curl -LsSf https://astral.sh/uv/install.sh | sh
33
+ ```
34
+
35
+ ### 2. Inicializar ambiente do projeto
36
+
37
+ Na raiz do repositório (onde está o `pyproject.toml`):
38
+
39
+ ```bash
40
+ uv sync --all-groups
41
+ ```
42
+
43
+ O `uv sync` cria `.venv/`, instala dependências fixadas em `uv.lock` e registra **`xploreai`** em modo editável a partir de `src/xploreai/`.
44
+
45
+ Para **criar um projeto Python novo** com uv (fora deste repo):
46
+
47
+ ```bash
48
+ uv init --python 3.12
49
+ uv add pandas # exemplo: adiciona dependencia e atualiza pyproject.toml + uv.lock
50
+ uv sync
51
+ ```
52
+
53
+ ### 3. Notebooks sem output no Git (nbstripout)
54
+
55
+ Este repo **nao usa** o pacote [`pre-commit`](https://pre-commit.com/) para notebooks. A limpeza e feita pelo **filtro Git** do [`nbstripout`](https://github.com/kynan/nbstripout) (ja dependencia em `pyproject.toml`).
56
+
57
+ **Uma vez por clone** (depois de `uv sync`), na raiz do repo:
58
+
59
+ ```bash
60
+ uv run nbstripout --install --attributes .gitattributes
61
+ ```
62
+
63
+ Isso registra o filtro em `.git/config` e versiona regras em `.gitattributes` (`*.ipynb filter=nbstripout`). No `git add`, outputs sao removidos do que entra no indice; no working tree voce pode continuar executando o notebook normalmente.
64
+
65
+ Conferir se esta ativo:
66
+
67
+ ```bash
68
+ uv run nbstripout --is-installed
69
+ uv run nbstripout --status
70
+ ```
71
+
72
+ Se recriar o `.venv` ou mudar o caminho do Python, reinstale o filtro:
73
+
74
+ ```bash
75
+ uv run nbstripout --install --attributes .gitattributes --force
76
+ ```
77
+
78
+ Limpar notebooks ja versionados (one-shot), se necessario:
79
+
80
+ ```bash
81
+ uv run nbstripout $(git ls-files '*.ipynb')
82
+ ```
83
+
84
+ Validar CI/local sem alterar arquivos:
85
+
86
+ ```bash
87
+ uv run nbstripout --verify $(git ls-files '*.ipynb')
88
+ ```
89
+
90
+ ### 4. Execução
91
+
92
+ Validação do setup:
93
+
94
+ ```bash
95
+ uv run xploreai
96
+ ```
97
+
98
+ Testes:
99
+
100
+ ```bash
101
+ uv run pytest
102
+ ```
103
+
104
+ Exemplo de cookbook:
105
+
106
+ ```bash
107
+ uv run python cookbook/01_langchain/prompt_template.py
108
+ ```
109
+
110
+ ## Versionamento semântico (Commitizen)
111
+
112
+ Commits seguem **[Conventional Commits](https://www.conventionalcommits.org/)** via [Commitizen](https://commitizen-tools.github.io/commitizen/). A versão do pacote vive em `pyproject.toml` (`[project].version`); o `cz bump` atualiza essa versão, o `CHANGELOG.md` e cria a tag Git `vX.Y.Z`.
113
+
114
+ ### Commits e push
115
+
116
+ ```bash
117
+ git add .
118
+ uv run cz commit
119
+ git push # sem publicação de nova versão
120
+ ```
121
+
122
+ | Tipo | Quando usar | Bump |
123
+ |------|-------------|------|
124
+ | `feat` | Nova funcionalidade | minor |
125
+ | `fix` | Correção de bug | patch |
126
+ | `docs` | Documentação | — |
127
+ | `refactor` | Refatoração | patch |
128
+ | `test` | Testes | — |
129
+ | `chore` | Manutenção | — |
130
+
131
+ ### Release e publicação
132
+
133
+ ```bash
134
+ uv run cz bump # bump de versão + CHANGELOG + tag
135
+ uv build # gera dist/*.whl
136
+ uv run --with xploreai --no-project -- python -c "import xploreai" # testar artefato
137
+ uv run --env-file .env --no-project -- uv publish # PyPI (token em UV_PUBLISH_TOKEN)
138
+ git push && git push --tags
139
+ ```
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "xploreai"
3
+ version = "0.2.0"
4
+ description = "Library and cookbook codes for AI Engineering Projects"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12,<3.13"
7
+ dependencies = ["pytest>=9.2.0"]
8
+
9
+ [[project.authors]]
10
+ name = "Daniel Dominguete"
11
+ email = "daniel.dominguete@gmail.com"
12
+
13
+ [project.scripts]
14
+ xploreai = "xploreai:main"
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.12.23,<0.13.0"]
18
+ build-backend = "uv_build"
19
+
20
+ [dependency-groups]
21
+ dev = ["commitizen>=4.19.3"]
22
+
23
+ [tool.commitizen]
24
+ name = "cz_conventional_commits"
25
+ version_provider = "pep621"
26
+ tag_format = "v$version"
27
+ update_changelog_on_bump = true
28
+ changelog_file = "CHANGELOG.md"
29
+ major_version_zero = true
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "xploreai"
3
+ version = "0.2.0"
4
+ description = "Library and cookbook codes for AI Engineering Projects"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Daniel Dominguete", email = "daniel.dominguete@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12,<3.13"
10
+ dependencies = [
11
+ "pytest>=9.2.0",
12
+ ]
13
+
14
+ [project.scripts]
15
+ xploreai = "xploreai:main"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.12.23,<0.13.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "commitizen>=4.19.3",
24
+ ]
25
+
26
+ [tool.commitizen]
27
+ name = "cz_conventional_commits"
28
+ version_provider = "pep621"
29
+ tag_format = "v$version"
30
+ update_changelog_on_bump = true
31
+ changelog_file = "CHANGELOG.md"
32
+ major_version_zero = true
@@ -0,0 +1,2 @@
1
+ def main() -> None:
2
+ print("Hello from xploreai!")
@@ -0,0 +1,20 @@
1
+ """
2
+ Xplore DS :: Localhost Tools Package
3
+ """
4
+
5
+ # importando as bibliotecas padrao
6
+ from pathlib import Path
7
+ import sys, os
8
+
9
+ # Configurando path para raiz do projeto e setup de reconhecimento da pasta da lib
10
+ project_folder = Path(__file__).resolve().parents[2]
11
+ sys.path.append(str(project_folder))
12
+
13
+ from xploredev.io.file import create_folder
14
+
15
+
16
+ class XploreDevLocalhost:
17
+ def __init__(self, run_folder: str) -> None:
18
+
19
+ # criando a pasta de runs caso não exista
20
+ create_folder(str(run_folder) + "/runs")
@@ -0,0 +1,211 @@
1
+ """
2
+ Xplore DEV :: Logging Tools Package
3
+
4
+ Structured logging for scripts and cookbook runs, including environment profiling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ import platform
12
+ import sys
13
+ import time
14
+ import warnings
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Any, Final
18
+
19
+ import psutil
20
+
21
+ from xploreds.io.xds_file import create_folder
22
+ from xploreds.utils.xds_memory import convert_bytes
23
+
24
+ PathLike = str | Path
25
+
26
+ _LOGGER_NAME: Final = "xploreds"
27
+ _LOG_FORMAT: Final = "%(asctime)s - %(levelname)s - %(message)s"
28
+ _LOG_DATE_FORMAT: Final = "%d-%m-%Y %H:%M:%S"
29
+ _RUN_TIMESTAMP_FORMAT: Final = "%y%m%d%H%M%S"
30
+
31
+ _SEPARATOR_TITLE: Final = "*" * 82
32
+ _SEPARATOR_SUBTITLE: Final = "=" * 82
33
+ _SEPARATOR_SECTION: Final = "-" * 82
34
+
35
+ _VALID_WARNING_ACTIONS: Final = frozenset(
36
+ {"error", "ignore", "always", "default", "module", "once"}
37
+ )
38
+
39
+
40
+ def _configure_python_warnings() -> None:
41
+ """Apply warning filter from ``PYTHON_WARNINGS`` env var (default: ``default``)."""
42
+ action = os.getenv("PYTHON_WARNINGS", "default")
43
+ if action not in _VALID_WARNING_ACTIONS:
44
+ action = "default"
45
+ warnings.filterwarnings(action)
46
+
47
+
48
+ def _log_bordered(logger: logging.Logger, message: str, separator: str) -> None:
49
+ """Log a message framed by the same separator line above and below."""
50
+ logger.info(separator)
51
+ logger.info(message)
52
+ logger.info(separator)
53
+
54
+
55
+ class XploreDSLogging:
56
+ """
57
+ Configure and write structured logs for a single script execution.
58
+
59
+ Each run creates a timestamped folder under ``runs/`` with a dedicated
60
+ ``.log`` file. Use wrapper methods (``info``, ``title``, ``section``) to
61
+ keep log output consistent across projects.
62
+
63
+ Args:
64
+ project_root: Root directory of the project.
65
+ script_name: Name or path of the script being executed (used in log paths).
66
+ """
67
+
68
+ def __init__(self, project_root: PathLike, script_name: PathLike) -> None:
69
+ self.project_root = Path(project_root)
70
+ script_path = Path(script_name)
71
+ self.script_name = script_path.name
72
+
73
+ self.dt_init = datetime.now()
74
+ self.dt_init_str = self.dt_init.strftime(_RUN_TIMESTAMP_FORMAT)
75
+ self.ts_init = time.time()
76
+
77
+ script_stem = script_path.stem
78
+ self.log_run = f"{script_stem}_{self.dt_init_str}"
79
+ self.log_path = self.project_root / "runs" / self.log_run
80
+ self.log_name = f"{self.log_run}.log"
81
+ self.log_file = self.log_path / self.log_name
82
+
83
+ create_folder(self.log_path)
84
+ self.logger = self._setup_logger()
85
+ _configure_python_warnings()
86
+
87
+ def _setup_logger(self) -> logging.Logger:
88
+ """Configure file and console handlers for the current run."""
89
+ logger = logging.getLogger(_LOGGER_NAME)
90
+ logger.setLevel(logging.INFO)
91
+ logger.handlers.clear()
92
+ logger.propagate = False
93
+
94
+ formatter = logging.Formatter(_LOG_FORMAT, datefmt=_LOG_DATE_FORMAT)
95
+
96
+ file_handler = logging.FileHandler(self.log_file)
97
+ stream_handler = logging.StreamHandler(sys.stderr)
98
+ for handler in (file_handler, stream_handler):
99
+ handler.setFormatter(formatter)
100
+ logger.addHandler(handler)
101
+
102
+ return logger
103
+
104
+ def info(self, message: str, *args: Any, **kwargs: Any) -> None:
105
+ """Log an information-level message."""
106
+ self.logger.info(message, *args, **kwargs)
107
+
108
+ def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
109
+ """Log a warning-level message."""
110
+ self.logger.warning(message, *args, **kwargs)
111
+
112
+ def error(self, message: str, *args: Any, **kwargs: Any) -> None:
113
+ """Log an error-level message."""
114
+ self.logger.error(message, *args, **kwargs)
115
+
116
+ def title(self, message: str) -> None:
117
+ """Log a top-level title with a heavy separator."""
118
+ _log_bordered(self.logger, message, _SEPARATOR_TITLE)
119
+
120
+ def subtitle(self, message: str) -> None:
121
+ """Log a subtitle with a medium separator."""
122
+ _log_bordered(self.logger, message, _SEPARATOR_SUBTITLE)
123
+
124
+ def section(self, message: str) -> None:
125
+ """Log a section header with a light separator."""
126
+ _log_bordered(self.logger, message, _SEPARATOR_SECTION)
127
+
128
+ def init_run(self) -> None:
129
+ """Log the start banner for the current run."""
130
+ _log_bordered(self.logger, f"Starting script: {self.log_run}", _SEPARATOR_TITLE)
131
+
132
+ def close_run(self) -> None:
133
+ """Log the closing banner with execution duration."""
134
+ ended_at = datetime.now()
135
+ exec_hours = (time.time() - self.ts_init) / 3600
136
+
137
+ self.logger.info(_SEPARATOR_TITLE)
138
+ self.logger.info("Execution finished script: %s", self.log_run)
139
+ self.logger.info("Conclusion at %s", ended_at.strftime("%Y-%m-%d %H:%M"))
140
+ self.logger.info("Execution time: %.2f hours", exec_hours)
141
+ self.logger.info(_SEPARATOR_TITLE)
142
+ self.logger.info("%74sXploreDS", "")
143
+ self.logger.info(_SEPARATOR_TITLE)
144
+
145
+ def log_environment_setup(self) -> None:
146
+ """
147
+ Log hardware, OS, Python, and run configuration details.
148
+
149
+ Note:
150
+ Requires ``psutil`` for memory and CPU reporting.
151
+ """
152
+ self.logger.info(_SEPARATOR_TITLE)
153
+ self.logger.info("Environment Setup")
154
+ self._log_machine_setup()
155
+ self._log_os_setup()
156
+ self._log_python_setup()
157
+ self._log_run_setup()
158
+ self.logger.info(_SEPARATOR_TITLE)
159
+
160
+ def _log_section(self, title: str) -> None:
161
+ """Log a labeled section with light separators."""
162
+ _log_bordered(self.logger, title, _SEPARATOR_SECTION)
163
+
164
+ def _log_machine_setup(self) -> None:
165
+ """Log CPU and memory details."""
166
+ self._log_section("Machine Setup")
167
+ self.logger.info("Machine: %s", platform.machine())
168
+ self.logger.info("Processor: %s", platform.processor())
169
+ self.logger.info("Physical cores: %s", psutil.cpu_count(logical=False))
170
+ self.logger.info("Total cores: %s", psutil.cpu_count(logical=True))
171
+ memory_gb = convert_bytes(psutil.virtual_memory().total, "GB")
172
+ self.logger.info("Physical memory: %.2fGB", memory_gb)
173
+
174
+ def _log_os_setup(self) -> None:
175
+ """Log operating system details."""
176
+ self._log_section("OS Setup")
177
+ self.logger.info("OS id: %s", platform.platform())
178
+ self.logger.info("OS System: %s", platform.system())
179
+ self.logger.info("OS Node Name: %s", platform.node())
180
+ self.logger.info("OS Release: %s", platform.release())
181
+
182
+ def _log_python_setup(self) -> None:
183
+ """Log Python interpreter details."""
184
+ self._log_section("Python Setup")
185
+ self.logger.info("Python version: %s", sys.version)
186
+ self.logger.info("Python path: %s", sys.executable)
187
+ self.logger.info("Python warnings: %s", os.getenv("PYTHON_WARNINGS"))
188
+
189
+ def _log_run_setup(self) -> None:
190
+ """Log paths and identifiers for the current run."""
191
+ self._log_section("Run Setup")
192
+ self.logger.info("Root folder: %s", self.project_root)
193
+ self.logger.info("Run name: %s", self.log_run)
194
+ self.logger.info("Artifacts folder: %s", self.log_path)
195
+
196
+
197
+ def create_logger(project_root: PathLike, script_path: PathLike) -> XploreDSLogging:
198
+ """
199
+ Create the run logger for the current script.
200
+
201
+ Args:
202
+ project_root: Root directory of the XploreDS project.
203
+ script_path: Path to the script file being executed.
204
+
205
+ Returns:
206
+ Configured ``XploreDSLogging`` instance.
207
+ """
208
+ return XploreDSLogging(
209
+ project_root=project_root,
210
+ script_name=script_path,
211
+ )
@@ -0,0 +1,48 @@
1
+ """
2
+ Xplore DEV :: Environment Utils Package
3
+
4
+ Utilities for locating project roots and resolving runtime paths.
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ PathLike = str | Path
10
+
11
+ DEFAULT_PROJECT_MARKER = "pyproject.toml"
12
+
13
+
14
+ def resolve_project_root(
15
+ start: PathLike | None = None,
16
+ *,
17
+ marker: str = DEFAULT_PROJECT_MARKER,
18
+ ) -> Path:
19
+ """
20
+ Locate a project root by walking parent directories for a marker file.
21
+
22
+ Args:
23
+ start: Directory to begin the search. Defaults to the current
24
+ working directory when ``None``.
25
+ marker: Filename that identifies the project root. Defaults to
26
+ ``pyproject.toml``.
27
+
28
+ Returns:
29
+ Absolute path to the directory containing the marker file.
30
+
31
+ Raises:
32
+ RuntimeError: When ``marker`` cannot be found in any parent folder.
33
+
34
+ Example:
35
+ From a cookbook script::
36
+
37
+ project_root = resolve_project_root(Path(__file__).resolve().parent)
38
+ """
39
+ current = Path(start).resolve() if start is not None else Path.cwd()
40
+
41
+ for path in (current, *current.parents):
42
+ if (path / marker).is_file():
43
+ return path
44
+
45
+ raise RuntimeError(
46
+ f"Could not find {marker!r} starting from {current}. "
47
+ "Run the script from within the project repository."
48
+ )
@@ -0,0 +1,39 @@
1
+ """
2
+ Xplore DS :: Memory Utils Package
3
+
4
+ Utilities for converting and expressing memory sizes in binary units.
5
+ """
6
+
7
+ from typing import Final
8
+
9
+ BYTES_PER_UNIT: Final[int] = 1024
10
+ BINARY_UNITS: Final[tuple[str, ...]] = ("B", "KB", "MB", "GB", "TB")
11
+ _UNIT_EXPONENT: Final[dict[str, int]] = {
12
+ unit: exponent for exponent, unit in enumerate(BINARY_UNITS)
13
+ }
14
+
15
+
16
+ def convert_bytes(size_bytes: int | float, to_unit: str = "GB") -> float:
17
+ """
18
+ Convert a byte count to the requested binary unit (KiB-based).
19
+
20
+ Args:
21
+ size_bytes: Size in bytes.
22
+ to_unit: Target unit. One of B, KB, MB, GB, or TB (case-insensitive).
23
+
24
+ Returns:
25
+ Size expressed in the requested unit.
26
+
27
+ Raises:
28
+ ValueError: When ``to_unit`` is not supported.
29
+
30
+ Example:
31
+ ``convert_bytes(1073741824, "GB")`` returns ``1.0``.
32
+ """
33
+ unit = to_unit.upper()
34
+ if unit not in _UNIT_EXPONENT:
35
+ supported = ", ".join(BINARY_UNITS)
36
+ raise ValueError(f"Invalid unit {to_unit!r}. Use one of: {supported}.")
37
+
38
+ exponent = _UNIT_EXPONENT[unit]
39
+ return float(size_bytes) / (BYTES_PER_UNIT**exponent)