fpptools 0.1.1__tar.gz → 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.
Files changed (36) hide show
  1. fpptools-0.2.0/.gitignore +119 -0
  2. fpptools-0.2.0/CHANGELOG.md +29 -0
  3. fpptools-0.2.0/PKG-INFO +129 -0
  4. fpptools-0.2.0/README.md +107 -0
  5. fpptools-0.2.0/fpptools/__init__.py +3 -0
  6. fpptools-0.2.0/fpptools/__main__.py +6 -0
  7. fpptools-0.2.0/fpptools/app.py +29 -0
  8. fpptools-0.2.0/fpptools/cli.py +38 -0
  9. fpptools-0.2.0/fpptools/commands/__init__.py +37 -0
  10. fpptools-0.2.0/fpptools/commands/config_cmd.py +36 -0
  11. fpptools-0.2.0/fpptools/commands/fit.py +140 -0
  12. fpptools-0.2.0/fpptools/commands/str.py +55 -0
  13. fpptools-0.2.0/fpptools/commands/wipe.py +122 -0
  14. fpptools-0.2.0/fpptools/config.py +148 -0
  15. fpptools-0.2.0/fpptools/db.py +85 -0
  16. fpptools-0.2.0/fpptools/errors.py +5 -0
  17. fpptools-0.2.0/fpptools/fpptools.py +5 -0
  18. fpptools-0.2.0/fpptools/output.py +45 -0
  19. fpptools-0.2.0/fpptools/quick.py +70 -0
  20. fpptools-0.2.0/fpptools/safe_write.py +76 -0
  21. fpptools-0.2.0/fpptools/strfile.py +202 -0
  22. fpptools-0.2.0/pyproject.toml +52 -0
  23. fpptools-0.2.0/scripts/build_exe.ps1 +11 -0
  24. fpptools-0.2.0/scripts/exe_estructuras.py +4 -0
  25. fpptools-0.2.0/scripts/exe_fpptools.py +4 -0
  26. fpptools-0.2.0/tests/__init__.py +0 -0
  27. fpptools-0.2.0/tests/conftest.py +80 -0
  28. fpptools-0.2.0/tests/test_fpptools.py +247 -0
  29. fpptools-0.1.1/PKG-INFO +0 -23
  30. fpptools-0.1.1/README.md +0 -35
  31. fpptools-0.1.1/fpptools/__init__.py +0 -1
  32. fpptools-0.1.1/fpptools/fittinglist_str.txt +0 -2956
  33. fpptools-0.1.1/fpptools/fpptools.py +0 -207
  34. fpptools-0.1.1/pyproject.toml +0 -31
  35. fpptools-0.1.1/setup.py +0 -37
  36. {fpptools-0.1.1 → fpptools-0.2.0}/LICENSE +0 -0
@@ -0,0 +1,119 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # PyInstaller
29
+ # Usually these files are written by a python script from a template
30
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .coverage
42
+ .coverage.*
43
+ .cache
44
+ nosetests.xml
45
+ coverage.xml
46
+ *.cover
47
+ .hypothesis/
48
+ .pytest_cache/
49
+
50
+ # Translations
51
+ *.mo
52
+ *.pot
53
+
54
+ # Django stuff:
55
+ *.log
56
+ local_settings.py
57
+ db.sqlite3
58
+
59
+ # Flask stuff:
60
+ instance/
61
+ .webassets-cache
62
+
63
+ # Scrapy stuff:
64
+ .scrapy
65
+
66
+ # Sphinx documentation
67
+ docs/_build/
68
+
69
+ # PyBuilder
70
+ target/
71
+
72
+ # Jupyter Notebook
73
+ .ipynb_checkpoints
74
+
75
+ # pyenv
76
+ .python-version
77
+
78
+ # celery beat schedule file
79
+ celerybeat-schedule
80
+
81
+ # SageMath parsed files
82
+ *.sage.py
83
+
84
+ # Environments
85
+ .env
86
+ .venv
87
+ env/
88
+ venv/
89
+ ENV/
90
+ env.bak/
91
+ venv.bak/
92
+
93
+ # Spyder project settings
94
+ .spyderproject
95
+ .spyproject
96
+
97
+ # Rope project settings
98
+ .ropeproject
99
+
100
+ # mkdocs documentation
101
+ /site
102
+
103
+ # mypy
104
+ .mypy_cache/
105
+
106
+ # fpptools runtime output
107
+ fpptools_backups/
108
+ *.fpptools.tmp
109
+
110
+ # standalone exes (scripts/build_exe.ps1); dist/ holds only the PyPI wheel/sdist
111
+ dist_exe/
112
+
113
+ # outputs fpptools writes in the working folder, and archives
114
+ *.7z
115
+ estructuras*.xlsx
116
+ structures.csv
117
+ structures.xlsx
118
+ fittings_*.csv
119
+ fittinglist_*.csv
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (unreleased)
4
+
5
+ Rewrite for current Python and FP Pro 11.
6
+
7
+ ### Added
8
+ - `fpptools str list`: structures with folder, name, unique code (NameStr) and description (decoded from Base64). CSV (`;`, UTF-8 BOM) or `.xlsx`, `--lang es`.
9
+ - `fpptools_estructuras.exe` / `fpptools-estructuras`: double-click export of structures to `estructuras.xlsx` next to the exe.
10
+ - `fpptools config`: show the active configuration (password hidden).
11
+ - `fpptools wipe panedefs` and `fpptools wipe kits`: simulate by default; `--apply` backs up every file first and writes a report.
12
+ - `--under`, `--match`, `--root` on every folder-scanning command; `--ini` / `FPPTOOLS_INI` on all commands.
13
+ - Tests with generic fixtures.
14
+
15
+ ### Changed
16
+ - `list_fittings` is now `fpptools fit list` (old syntax kept as a hidden, deprecated alias). Output is CSV with `;`; columns Folder, File, Fitting, Source, Note.
17
+ - Job selection uses `--job NAME` (prompted if missing).
18
+ - Rewritten files keep FP Pro's format: original XML declaration, windows-1252, CRLF.
19
+ - Packaging: uv + hatchling, Python >= 3.10, click 8, lxml 5+, PyMySQL 1.x, pyodbc 5 (Windows only).
20
+
21
+ ### Fixed
22
+ - Importing fpptools no longer reads FP_PRO.INI or connects to the database (`--help` works without FP Pro).
23
+ - MySQL now uses `DB_PORT` and `DB_CHARSET` from FP_PRO.INI (defaults 3306 and latin1 when missing or empty).
24
+ - Access kits query no longer exhausts its cursor after the first file.
25
+ - Clean error messages instead of tracebacks when the INI, folders or database are missing.
26
+
27
+ ## 0.1.1
28
+
29
+ - `list_fittings` for str/blk/job/jobs.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.5
2
+ Name: fpptools
3
+ Version: 0.2.0
4
+ Summary: Administration and debugging CLI for emmegisoft FP Pro (structures, blocks, jobs)
5
+ Project-URL: Repository, https://github.com/andoresuperesu/fpptools
6
+ Author-email: Andres Perez <andresperezcera@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Environment :: Console
10
+ Classifier: Environment :: Win32 (MS Windows)
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: Topic :: Utilities
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: click>=8.1
17
+ Requires-Dist: lxml>=5.0
18
+ Requires-Dist: openpyxl>=3.1
19
+ Requires-Dist: pymysql>=1.1
20
+ Requires-Dist: pyodbc>=5.0; sys_platform == 'win32'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # fpptools
24
+
25
+ Administration and debugging command line for emmegisoft **FP Pro**: structures (`.STR`), blocks (`.BLK`) and job records.
26
+ It works on the configuration that is **active in FP Pro right now** (read from `%APPDATA%\Emmegisoft\FP_PRO\FP_PRO.INI`).
27
+
28
+ *Guía rápida en español más abajo.*
29
+
30
+ ## Install
31
+
32
+ ```powershell
33
+ pip install fpptools # or: uv tool install fpptools
34
+ fpptools --help
35
+ ```
36
+
37
+ Standalone exes (no Python needed) are built with `scripts\build_exe.ps1`:
38
+
39
+ - `fpptools.exe`: the full command line.
40
+ - `fpptools_estructuras.exe`: **double-click** to export the structures of the active configuration to `estructuras.xlsx` next to the exe (same as `fpptools str list --lang es -o estructuras.xlsx`). If that file is open in Excel, it writes `estructuras_<date_time>.xlsx` instead. Also installed as the `fpptools-estructuras` command.
41
+
42
+ ## Commands
43
+
44
+ | Command | What it does |
45
+ |---|---|
46
+ | `fpptools config` | Show the active configuration: folders, database (password hidden) |
47
+ | `fpptools str list` | List structures: folder, name, unique code, description |
48
+ | `fpptools fit list` | List fittings used in structures, blocks, one job or all jobs (kits expanded from the database) |
49
+ | `fpptools wipe panedefs` | Remove glass fittings, glass DXF and pane table from every pane of the structures |
50
+ | `fpptools wipe kits` | Remove every kit from structures or blocks |
51
+
52
+ Every command has `--help`.
53
+
54
+ ### Choosing files
55
+
56
+ Commands that read STR/BLK folders accept:
57
+
58
+ - `--under FOLDER`: only that folder of the tree, e.g. `--under 019_SERIE_22_PESADO`
59
+ - `--match PATTERN`: file name pattern without extension, e.g. `--match "VTA_*"` (case-insensitive)
60
+ - `--root FOLDER`: another folder instead of the one in FP_PRO.INI
61
+
62
+ ### Output
63
+
64
+ `--out file.csv` (default), `--out file.xlsx`, or `--out -` for the console.
65
+ CSV files use `;` and UTF-8 with BOM, so Excel opens them correctly with accents. `--lang es` writes Spanish headers.
66
+
67
+ ### Commands that modify files: simulate first
68
+
69
+ `wipe` commands **only simulate** by default: they report what would change and write nothing.
70
+ Add `--apply` to write. Before writing, every file is copied to `--backup-dir` (default `.\fpptools_backups\<date_time>\`, same sub-folders) together with a `report.txt`. To undo, copy the backed-up files back.
71
+
72
+ ```powershell
73
+ fpptools wipe panedefs --under "LINEA X" # simulation
74
+ fpptools wipe panedefs --under "LINEA X" --apply # backup + write
75
+ ```
76
+
77
+ Close the structures in FP Pro before applying.
78
+
79
+ ### Structure fields
80
+
81
+ | Column | Where it comes from |
82
+ |---|---|
83
+ | Folder | Folder of the file inside STR_PATH (the tree in FP Pro's structure browser) |
84
+ | Name | File name without `.STR` (the "Nombre" field in FP Pro) |
85
+ | Unique code | `FP_TREE/@NameStr` ("nombre unívoco" in FP Pro) |
86
+ | Description | `FP_TREE/FP_DESCRIPTION`, stored as Base64 inside the file |
87
+
88
+ ### Database
89
+
90
+ `fit list` reads kits from the FP Pro database of the active configuration: MySQL (`CONN_STR_FP_PRO=1`, settings in `[MYSQL]`) or Access (`Fp_Pro32.MDB` in `MDB_PATH`). Queries are read-only.
91
+ Access needs the Microsoft Access ODBC driver with the same bitness (32/64-bit) as Python. Use `--no-db` to skip the database.
92
+
93
+ ### Other options
94
+
95
+ - `--ini PATH` (or environment variable `FPPTOOLS_INI`): use another FP_PRO.INI.
96
+ - `fpptools list_fittings --searchtype str` (0.1 syntax) still works but is deprecated.
97
+
98
+ ---
99
+
100
+ ## Guía rápida (español)
101
+
102
+ `fpptools` trabaja sobre la configuración **activa en FP Pro**.
103
+
104
+ **Exportar nombre y descripción de las estructuras:**
105
+
106
+ ```powershell
107
+ fpptools str list --lang es -o estructuras.xlsx
108
+ fpptools str list --under 019_SERIE_22_PESADO --lang es -o serie22.csv
109
+ ```
110
+
111
+ Columnas: Carpeta, Nombre (nombre del archivo), Nombre único (nombre unívoco), Descripción.
112
+
113
+ **Sin línea de comandos:** doble clic en `fpptools_estructuras.exe`. Crea `estructuras.xlsx` en la misma carpeta del programa (si está abierto en Excel, crea `estructuras_<fecha_hora>.xlsx`). El programa debe estar en una carpeta donde se pueda escribir (no en `C:\Program Files`).
114
+
115
+ **Comandos que modifican archivos** (`wipe`): por defecto solo **simulan** y muestran qué cambiarían. Con `--apply` primero copian los originales a `fpptools_backups\<fecha_hora>\` y luego escriben. Cierre las estructuras en FP Pro antes de aplicar.
116
+
117
+ `fpptools config` muestra qué configuración, carpetas y base de datos está usando.
118
+
119
+ ## Development
120
+
121
+ ```powershell
122
+ uv sync
123
+ uv run pytest
124
+ uv run fpptools --help
125
+ uv build ; uv publish # PyPI
126
+ .\scripts\build_exe.ps1 # dist_exe\fpptools.exe, dist_exe\fpptools_estructuras.exe (not in dist\: that is PyPI's)
127
+ ```
128
+
129
+ License: MIT.
@@ -0,0 +1,107 @@
1
+ # fpptools
2
+
3
+ Administration and debugging command line for emmegisoft **FP Pro**: structures (`.STR`), blocks (`.BLK`) and job records.
4
+ It works on the configuration that is **active in FP Pro right now** (read from `%APPDATA%\Emmegisoft\FP_PRO\FP_PRO.INI`).
5
+
6
+ *Guía rápida en español más abajo.*
7
+
8
+ ## Install
9
+
10
+ ```powershell
11
+ pip install fpptools # or: uv tool install fpptools
12
+ fpptools --help
13
+ ```
14
+
15
+ Standalone exes (no Python needed) are built with `scripts\build_exe.ps1`:
16
+
17
+ - `fpptools.exe`: the full command line.
18
+ - `fpptools_estructuras.exe`: **double-click** to export the structures of the active configuration to `estructuras.xlsx` next to the exe (same as `fpptools str list --lang es -o estructuras.xlsx`). If that file is open in Excel, it writes `estructuras_<date_time>.xlsx` instead. Also installed as the `fpptools-estructuras` command.
19
+
20
+ ## Commands
21
+
22
+ | Command | What it does |
23
+ |---|---|
24
+ | `fpptools config` | Show the active configuration: folders, database (password hidden) |
25
+ | `fpptools str list` | List structures: folder, name, unique code, description |
26
+ | `fpptools fit list` | List fittings used in structures, blocks, one job or all jobs (kits expanded from the database) |
27
+ | `fpptools wipe panedefs` | Remove glass fittings, glass DXF and pane table from every pane of the structures |
28
+ | `fpptools wipe kits` | Remove every kit from structures or blocks |
29
+
30
+ Every command has `--help`.
31
+
32
+ ### Choosing files
33
+
34
+ Commands that read STR/BLK folders accept:
35
+
36
+ - `--under FOLDER`: only that folder of the tree, e.g. `--under 019_SERIE_22_PESADO`
37
+ - `--match PATTERN`: file name pattern without extension, e.g. `--match "VTA_*"` (case-insensitive)
38
+ - `--root FOLDER`: another folder instead of the one in FP_PRO.INI
39
+
40
+ ### Output
41
+
42
+ `--out file.csv` (default), `--out file.xlsx`, or `--out -` for the console.
43
+ CSV files use `;` and UTF-8 with BOM, so Excel opens them correctly with accents. `--lang es` writes Spanish headers.
44
+
45
+ ### Commands that modify files: simulate first
46
+
47
+ `wipe` commands **only simulate** by default: they report what would change and write nothing.
48
+ Add `--apply` to write. Before writing, every file is copied to `--backup-dir` (default `.\fpptools_backups\<date_time>\`, same sub-folders) together with a `report.txt`. To undo, copy the backed-up files back.
49
+
50
+ ```powershell
51
+ fpptools wipe panedefs --under "LINEA X" # simulation
52
+ fpptools wipe panedefs --under "LINEA X" --apply # backup + write
53
+ ```
54
+
55
+ Close the structures in FP Pro before applying.
56
+
57
+ ### Structure fields
58
+
59
+ | Column | Where it comes from |
60
+ |---|---|
61
+ | Folder | Folder of the file inside STR_PATH (the tree in FP Pro's structure browser) |
62
+ | Name | File name without `.STR` (the "Nombre" field in FP Pro) |
63
+ | Unique code | `FP_TREE/@NameStr` ("nombre unívoco" in FP Pro) |
64
+ | Description | `FP_TREE/FP_DESCRIPTION`, stored as Base64 inside the file |
65
+
66
+ ### Database
67
+
68
+ `fit list` reads kits from the FP Pro database of the active configuration: MySQL (`CONN_STR_FP_PRO=1`, settings in `[MYSQL]`) or Access (`Fp_Pro32.MDB` in `MDB_PATH`). Queries are read-only.
69
+ Access needs the Microsoft Access ODBC driver with the same bitness (32/64-bit) as Python. Use `--no-db` to skip the database.
70
+
71
+ ### Other options
72
+
73
+ - `--ini PATH` (or environment variable `FPPTOOLS_INI`): use another FP_PRO.INI.
74
+ - `fpptools list_fittings --searchtype str` (0.1 syntax) still works but is deprecated.
75
+
76
+ ---
77
+
78
+ ## Guía rápida (español)
79
+
80
+ `fpptools` trabaja sobre la configuración **activa en FP Pro**.
81
+
82
+ **Exportar nombre y descripción de las estructuras:**
83
+
84
+ ```powershell
85
+ fpptools str list --lang es -o estructuras.xlsx
86
+ fpptools str list --under 019_SERIE_22_PESADO --lang es -o serie22.csv
87
+ ```
88
+
89
+ Columnas: Carpeta, Nombre (nombre del archivo), Nombre único (nombre unívoco), Descripción.
90
+
91
+ **Sin línea de comandos:** doble clic en `fpptools_estructuras.exe`. Crea `estructuras.xlsx` en la misma carpeta del programa (si está abierto en Excel, crea `estructuras_<fecha_hora>.xlsx`). El programa debe estar en una carpeta donde se pueda escribir (no en `C:\Program Files`).
92
+
93
+ **Comandos que modifican archivos** (`wipe`): por defecto solo **simulan** y muestran qué cambiarían. Con `--apply` primero copian los originales a `fpptools_backups\<fecha_hora>\` y luego escriben. Cierre las estructuras en FP Pro antes de aplicar.
94
+
95
+ `fpptools config` muestra qué configuración, carpetas y base de datos está usando.
96
+
97
+ ## Development
98
+
99
+ ```powershell
100
+ uv sync
101
+ uv run pytest
102
+ uv run fpptools --help
103
+ uv build ; uv publish # PyPI
104
+ .\scripts\build_exe.ps1 # dist_exe\fpptools.exe, dist_exe\fpptools_estructuras.exe (not in dist\: that is PyPI's)
105
+ ```
106
+
107
+ License: MIT.
@@ -0,0 +1,3 @@
1
+ """fpptools: administration and debugging CLI for emmegisoft FP Pro."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,6 @@
1
+ """Allows `python -m fpptools` and is the PyInstaller entry script."""
2
+
3
+ from fpptools.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,29 @@
1
+ """Shared CLI state passed to every command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ from fpptools import config
10
+
11
+
12
+ class App:
13
+ """The FP Pro configuration is loaded only when a command needs it."""
14
+
15
+ def __init__(self, ini: Path | None):
16
+ self._ini = ini
17
+ self._cfg: config.FppConfig | None = None
18
+
19
+ @property
20
+ def cfg(self) -> config.FppConfig:
21
+ if self._cfg is None:
22
+ self._cfg = config.load(self._ini)
23
+ return self._cfg
24
+
25
+ def root(self, kind: str, override: Path | None) -> Path:
26
+ return Path(override) if override else self.cfg.root_for(kind)
27
+
28
+
29
+ pass_app = click.make_pass_decorator(App)
@@ -0,0 +1,38 @@
1
+ """fpptools command line: `fpptools <group> <command>`.
2
+
3
+ Groups: config, str, fit, wipe. Run `fpptools <group> --help` for details.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import click
11
+
12
+ from fpptools import __version__, config
13
+ from fpptools.app import App
14
+ from fpptools.commands import config_cmd, fit, str as str_cmd, wipe
15
+
16
+
17
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
18
+ @click.version_option(__version__, prog_name="fpptools")
19
+ @click.option("--ini", type=click.Path(dir_okay=False, path_type=Path), envvar=config.ENV_INI,
20
+ help="FP_PRO.INI to use. Default: the configuration FP Pro is using now.")
21
+ @click.pass_context
22
+ def cli(ctx: click.Context, ini: Path | None):
23
+ """Administration and debugging tools for emmegisoft FP Pro.
24
+
25
+ Works on the configuration currently active in FP Pro (FP_PRO.INI).
26
+ """
27
+ ctx.obj = App(ini)
28
+
29
+
30
+ cli.add_command(config_cmd.config_show)
31
+ cli.add_command(str_cmd.str_group)
32
+ cli.add_command(fit.fit_group)
33
+ cli.add_command(fit.legacy_list_fittings)
34
+ cli.add_command(wipe.wipe_group)
35
+
36
+
37
+ def main():
38
+ cli(prog_name="fpptools")
@@ -0,0 +1,37 @@
1
+ """CLI command groups. Shared options live here."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ HEADERS = {
10
+ "en": {"folder": "Folder", "name": "Name", "unique": "Unique code", "description": "Description",
11
+ "file": "File", "fitting": "Fitting", "source": "Source", "note": "Note", "error": "Error",
12
+ "job": "Job", "item": "Item"},
13
+ "es": {"folder": "Carpeta", "name": "Nombre", "unique": "Nombre único", "description": "Descripción",
14
+ "file": "Archivo", "fitting": "Accesorio", "source": "Origen", "note": "Nota", "error": "Error",
15
+ "job": "Trabajo", "item": "Ítem"},
16
+ }
17
+
18
+
19
+ def scope_options(f):
20
+ """--root / --under / --match, shared by every command that walks STR/BLK folders."""
21
+ f = click.option("--match", metavar="PATTERN",
22
+ help="Only files whose name (no extension) matches, e.g. 'VTA_*'. Case-insensitive.")(f)
23
+ f = click.option("--under", metavar="FOLDER",
24
+ help="Only inside this folder, relative to the root, e.g. '019_SERIE_22_PESADO'.")(f)
25
+ f = click.option("--root", type=click.Path(file_okay=False, exists=True, path_type=Path),
26
+ help="Folder to scan instead of the one in FP_PRO.INI.")(f)
27
+ return f
28
+
29
+
30
+ def lang_option(f):
31
+ return click.option("--lang", type=click.Choice(["en", "es"]), default="en", show_default=True,
32
+ help="Language of the column headers.")(f)
33
+
34
+
35
+ def out_option(default: str):
36
+ return click.option("--out", "-o", default=default, show_default=True,
37
+ help="Output file (.csv or .xlsx), or '-' for the console.")
@@ -0,0 +1,36 @@
1
+ """`fpptools config`: show which FP Pro configuration fpptools will use."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from fpptools.app import App, pass_app
8
+
9
+
10
+ @click.command("config")
11
+ @pass_app
12
+ def config_show(app: App):
13
+ """Show the active FP Pro configuration (password hidden)."""
14
+ cfg = app.cfg
15
+
16
+ def line(label: str, value) -> None:
17
+ click.echo(f"{label:<16}{value}")
18
+
19
+ def folder(key: str) -> str:
20
+ value = cfg.get("SYSTEM", key)
21
+ if not value:
22
+ return "(not set)"
23
+ from pathlib import Path
24
+ return f"{value}" + ("" if Path(value).is_dir() else " [not found]")
25
+
26
+ line("INI", cfg.ini_path)
27
+ line("Configuration", cfg.name or "(unknown)")
28
+ line("STR_PATH", folder("STR_PATH"))
29
+ line("BLK_PATH", folder("BLK_PATH"))
30
+ line("JOB_PATH", folder("JOB_PATH"))
31
+ line("CONFIGS_PATH", folder("CONFIGS_PATH"))
32
+ if cfg.db_kind == "mysql":
33
+ s = cfg.mysql
34
+ line("Database", f"MySQL {s.host}:{s.port}/{s.database} (user {s.user}, charset {s.charset})")
35
+ else:
36
+ line("Database", f"Access {cfg.mdb_file}" + ("" if cfg.mdb_file.is_file() else " [not found]"))
@@ -0,0 +1,140 @@
1
+ """`fpptools fit ...`: fittings used in structures, blocks and jobs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from lxml import etree
10
+
11
+ from fpptools import db as dbmod
12
+ from fpptools import strfile
13
+ from fpptools.app import App, pass_app
14
+ from fpptools.commands import HEADERS, lang_option, scope_options
15
+ from fpptools.errors import FppToolsError
16
+ from fpptools.output import write_table
17
+
18
+ JOBRECORD = "_JOBRECORD"
19
+
20
+
21
+ def _origin(el) -> str:
22
+ parent = el.getparent()
23
+ return f"{parent.tag}:{parent.get('Code', '')}" if parent is not None else ""
24
+
25
+
26
+ def fittings_in_design(path: Path, kits: dict[str, list[str]] | None) -> list[tuple[str, str, str]]:
27
+ """(fitting code, source, note) for one STR/BLK. Kits are expanded from the database."""
28
+ tree = etree.parse(str(path), etree.XMLParser(resolve_entities=False, huge_tree=True))
29
+ rows = []
30
+ for fit in tree.iter("FP_FITTING"):
31
+ parent = fit.getparent()
32
+ if parent is not None and parent.tag == "FP_KIT":
33
+ continue # kit content comes from the database
34
+ rows.append((fit.get("Code", ""), _origin(fit), ""))
35
+ for kit in tree.iter("FP_KIT"):
36
+ name = kit.get("Code", "")
37
+ if kits is None:
38
+ rows.append(("", f"FP_KIT:{name}", "kit not expanded (--no-db)"))
39
+ elif name in kits:
40
+ rows.extend((code, f"FP_KIT:{name}", "") for code in kits[name])
41
+ else:
42
+ rows.append(("", f"FP_KIT:{name}", "kit not found in database"))
43
+ return rows
44
+
45
+
46
+ def job_files(root: Path, job: str | None) -> list[Path]:
47
+ found = []
48
+ for dirpath, dirnames, filenames in os.walk(root):
49
+ dirnames.sort(key=str.casefold)
50
+ for name in sorted(filenames, key=str.casefold):
51
+ upper = name.upper()
52
+ if not upper.endswith(".XML") or JOBRECORD not in upper:
53
+ continue
54
+ if job and not upper.startswith(job.upper() + JOBRECORD):
55
+ continue
56
+ found.append(Path(dirpath) / name)
57
+ return found
58
+
59
+
60
+ def job_and_item(path: Path) -> tuple[str, str]:
61
+ stem = path.stem
62
+ job, _, rest = stem.upper().partition(JOBRECORD)
63
+ item = stem[len(job) + len(JOBRECORD):].lstrip("_") or stem[-2:]
64
+ return stem[: len(job)], item
65
+
66
+
67
+ @click.group("fit")
68
+ def fit_group():
69
+ """Fittings (accessories)."""
70
+
71
+
72
+ @fit_group.command("list")
73
+ @click.option("--source", type=click.Choice(["str", "blk", "job", "jobs"]), default="str", show_default=True,
74
+ help="Structures, blocks, one job (--job) or all jobs.")
75
+ @click.option("--job", metavar="NAME", help="Job name for --source job (asked if missing).")
76
+ @click.option("--no-db", is_flag=True, help="Do not read the database; kits are listed but not expanded.")
77
+ @scope_options
78
+ @click.option("--out", "-o", default=None, help="Output file (.csv or .xlsx), or '-' for the console. "
79
+ "[default: fittings_<source>.csv]")
80
+ @lang_option
81
+ @pass_app
82
+ def fit_list(app: App, source: str, job: str | None, no_db: bool, root: Path | None, under: str | None,
83
+ match: str | None, out: str | None, lang: str):
84
+ """List the fittings used in structures, blocks or job records.
85
+
86
+ Fittings inside kits are taken from the database (kit / dettagliokit).
87
+ """
88
+ h = HEADERS[lang]
89
+ out = out or f"fittings_{source}.csv"
90
+ rows: list[list[str]] = []
91
+ if source in ("job", "jobs"):
92
+ if source == "job" and not job:
93
+ job = click.prompt("Job name")
94
+ base = app.root("job", root)
95
+ files = job_files(base, job if source == "job" else None)
96
+ if not files:
97
+ raise FppToolsError(f"No job records found in {base}" + (f" for job {job}" if job else ""))
98
+ headers = [h["job"], h["item"], h["fitting"], h["source"]]
99
+ for f in files:
100
+ j, item = job_and_item(f)
101
+ tree = etree.parse(str(f), etree.XMLParser(resolve_entities=False, huge_tree=True))
102
+ rows.extend([j, item, el.get("Code", ""), _origin(el)] for el in tree.iter("FP_FITTING"))
103
+ else:
104
+ base = app.root(source, root)
105
+ files = strfile.find_files(base, strfile.EXTENSIONS[source], under, match)
106
+ kits = None
107
+ if not no_db:
108
+ database = dbmod.ReadOnlyDB(app.cfg)
109
+ try:
110
+ kits = dbmod.kit_fittings(database)
111
+ finally:
112
+ database.close()
113
+ headers = [h["folder"], h["file"], h["fitting"], h["source"], h["note"]]
114
+ with click.progressbar(files, label=f"Reading {source.upper()} files",
115
+ file=click.get_text_stream("stderr"), show_pos=True) as bar:
116
+ for f in bar:
117
+ folder = strfile.rel_folder(f, base)
118
+ try:
119
+ found = fittings_in_design(f, kits)
120
+ except etree.XMLSyntaxError as exc:
121
+ click.echo(f"\nwarning: {f}: cannot read: {exc}", err=True)
122
+ rows.append([folder, f.name, "", "", f"cannot read: {exc}"])
123
+ continue
124
+ rows.extend([folder, f.name, code, src, note] for code, src, note in found)
125
+ where = write_table(headers, rows, out)
126
+ click.echo(f"{len(rows)} row(s) from {len(files)} file(s) -> {where}", err=True)
127
+
128
+
129
+ @click.command("list_fittings", hidden=True)
130
+ @click.option("--searchtype", type=click.Choice(["str", "blk", "job", "jobs"]), default="str")
131
+ @click.option("--tofile", type=bool, default=True)
132
+ @click.option("--verbose", type=bool, default=False)
133
+ @click.pass_context
134
+ def legacy_list_fittings(ctx: click.Context, searchtype: str, tofile: bool, verbose: bool):
135
+ """fpptools 0.1 syntax; use `fpptools fit list`."""
136
+ click.echo("note: list_fittings is deprecated, use `fpptools fit list`.", err=True)
137
+ if tofile:
138
+ ctx.invoke(fit_list, source=searchtype, out=f"fittinglist_{searchtype}.csv")
139
+ if verbose or not tofile:
140
+ ctx.invoke(fit_list, source=searchtype, out="-")