Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
194 lines
8.2 KiB
Markdown
194 lines
8.2 KiB
Markdown
---
|
|
phase: 05-package-export
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- imptune/api/packages.py
|
|
- imptune/main.py
|
|
- tests/test_packages.py
|
|
autonomous: true
|
|
requirements: [PKG-01, PKG-02, PKG-03]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "GET /printers/{id}/packages/ninja returns a ZIP containing install.ps1 and drivers/ subfolder"
|
|
- "GET /printers/{id}/packages/intunewin returns a valid .intunewin file with correct Content-Disposition"
|
|
- "Both endpoints return 404 for missing printer, 422 for missing/invalid driver"
|
|
- "NinjaRMM ZIP uses DEFLATE compression and has printer-name-based folder structure"
|
|
- ".intunewin is built using Python-native build_intunewin() with no subprocess calls"
|
|
artifacts:
|
|
- path: "imptune/api/packages.py"
|
|
provides: "Package download endpoints for NinjaRMM ZIP and .intunewin"
|
|
exports: ["router"]
|
|
- path: "tests/test_packages.py"
|
|
provides: "Integration tests for both export endpoints"
|
|
contains: "TestNinjaDownload"
|
|
key_links:
|
|
- from: "imptune/api/packages.py"
|
|
to: "imptune/generators/script_generator.py"
|
|
via: "render_install, render_uninstall, render_detect"
|
|
pattern: "from imptune\\.generators\\.script_generator import"
|
|
- from: "imptune/api/packages.py"
|
|
to: "imptune/generators/intunewin_builder.py"
|
|
via: "build_intunewin(source_dir, setup_file, output_path)"
|
|
pattern: "from imptune\\.generators\\.intunewin_builder import build_intunewin"
|
|
- from: "imptune/main.py"
|
|
to: "imptune/api/packages.py"
|
|
via: "app.include_router(packages.router)"
|
|
pattern: "include_router.*packages"
|
|
---
|
|
|
|
<objective>
|
|
Create the two package export API endpoints: NinjaRMM ZIP download and .intunewin download. Both serve binary file responses for a given printer configuration.
|
|
|
|
Purpose: PKG-01/PKG-02/PKG-03 -- Users can download deployment-ready packages in either format with one click.
|
|
Output: `imptune/api/packages.py` with two GET endpoints, registered in main.py, with integration tests.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
|
|
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/05-package-export/05-RESEARCH.md
|
|
|
|
@imptune/api/scripts.py
|
|
@imptune/generators/intunewin_builder.py
|
|
@imptune/generators/script_generator.py
|
|
@imptune/config.py
|
|
@imptune/db/models.py
|
|
@imptune/main.py
|
|
@tests/conftest.py
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts the executor needs. -->
|
|
|
|
From imptune/api/scripts.py:
|
|
```python
|
|
def _get_printer_and_driver(printer_id: int):
|
|
"""Returns (printer, driver, driver_name), None on success
|
|
or None, PlainTextResponse on error (404/422)."""
|
|
```
|
|
|
|
From imptune/generators/script_generator.py:
|
|
```python
|
|
def render_install(printer_name, ip_address, port_name, driver_name,
|
|
inf_filename, duplex_mode, color_mode, paper_size, collate) -> str: ...
|
|
def render_uninstall(printer_name, driver_name, port_name) -> str: ...
|
|
def render_detect(printer_name) -> str: ...
|
|
```
|
|
|
|
From imptune/generators/intunewin_builder.py:
|
|
```python
|
|
def build_intunewin(source_dir: str, setup_file: str, output_path: str) -> None:
|
|
"""Build a .intunewin file from source_dir, with setup_file as entry point."""
|
|
```
|
|
|
|
From imptune/config.py:
|
|
```python
|
|
DATA_DIR = os.environ.get("DATA_DIR", "/data")
|
|
DRIVERS_DIR = str(Path(DATA_DIR) / "drivers")
|
|
```
|
|
|
|
From imptune/db/models.py:
|
|
```python
|
|
class Driver(BaseModel):
|
|
sha256 = CharField(unique=True, index=True)
|
|
original_filename = CharField()
|
|
driver_desc = CharField(null=True) # JSON list of driver names
|
|
inf_filename = CharField(null=True)
|
|
...
|
|
|
|
class Printer(BaseModel):
|
|
name = CharField()
|
|
ip_address = CharField()
|
|
port_name = CharField()
|
|
driver = ForeignKeyField(Driver, null=True, backref="printers")
|
|
duplex_mode = CharField(default="OneSided")
|
|
color_mode = BooleanField(default=True)
|
|
paper_size = CharField(default="A4")
|
|
collate = BooleanField(default=True)
|
|
...
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Package export endpoints with TDD</name>
|
|
<files>imptune/api/packages.py, imptune/main.py, tests/test_packages.py</files>
|
|
<behavior>
|
|
- TestNinjaDownload::test_returns_zip: GET /printers/{id}/packages/ninja returns 200, media_type application/zip, Content-Disposition with filename
|
|
- TestNinjaDownload::test_zip_contains_install_script: Response ZIP contains {safe_name}/install.ps1
|
|
- TestNinjaDownload::test_zip_contains_driver_files: Response ZIP contains {safe_name}/drivers/ with files from driver ZIP
|
|
- TestNinjaDownload::test_404_missing_printer: Returns 404 for nonexistent printer_id
|
|
- TestNinjaDownload::test_422_no_driver: Returns 422 for printer with no assigned driver
|
|
- TestIntunewinDownload::test_returns_intunewin: GET /printers/{id}/packages/intunewin returns 200, media_type application/octet-stream, Content-Disposition with .intunewin extension
|
|
- TestIntunewinDownload::test_intunewin_is_valid_zip: Response content is a valid outer ZIP with IntuneWinPackage/ structure
|
|
- TestIntunewinDownload::test_404_missing_printer: Returns 404 for nonexistent printer_id
|
|
- TestIntunewinDownload::test_422_no_driver: Returns 422 for printer with no assigned driver
|
|
</behavior>
|
|
<action>
|
|
1. Create `tests/test_packages.py` with RED tests first. Test fixtures: create a Driver record with a real small ZIP file on disk (use tmp_data_dir from conftest), create a Printer record linked to it. Use the `client` fixture from conftest.py.
|
|
|
|
2. Create `imptune/api/packages.py` with `router = APIRouter(prefix="/printers")`:
|
|
|
|
**NinjaRMM endpoint** `GET /{printer_id}/packages/ninja`:
|
|
- Reuse `_get_printer_and_driver()` pattern from scripts.py (copy the helper into packages.py or import — prefer copy since it's small and keeps the module self-contained)
|
|
- Call `render_install(...)` with all printer/driver params
|
|
- Build ZIP in-memory with `io.BytesIO` + `zipfile.ZipFile`:
|
|
- `{safe_name}/install.ps1` with rendered script
|
|
- `{safe_name}/drivers/{member}` for each file in the driver ZIP on disk
|
|
- `safe_name = printer.name.replace(" ", "_")`
|
|
- Return `Response(content=buf.getvalue(), media_type="application/zip", headers={"Content-Disposition": f'attachment; filename="{safe_name}_ninja.zip"'})`
|
|
|
|
**Intunewin endpoint** `GET /{printer_id}/packages/intunewin`:
|
|
- Same printer/driver validation via `_get_printer_and_driver()`
|
|
- Use `tempfile.TemporaryDirectory(prefix="imptune_")` as context manager (auto-cleanup, per RESEARCH pitfall 1)
|
|
- Write `install.ps1`, `uninstall.ps1`, `detect.ps1` into tmpdir
|
|
- Extract driver ZIP contents into `tmpdir/drivers/`
|
|
- Call `build_intunewin(tmpdir, "install.ps1", os.path.join(tmpdir, "out.intunewin"))`
|
|
- Read output file bytes and return as `Response(content=..., media_type="application/octet-stream", headers={"Content-Disposition": ...})`
|
|
- Check driver file exists on disk before proceeding (per RESEARCH pitfall 3), return 422 if missing
|
|
|
|
3. Register router in `imptune/main.py`:
|
|
- Add `from imptune.api import packages` to imports
|
|
- Add `app.include_router(packages.router)` after scripts router
|
|
|
|
4. Run tests GREEN.
|
|
</action>
|
|
<verify>
|
|
<automated>pytest tests/test_packages.py -x</automated>
|
|
</verify>
|
|
<done>
|
|
- NinjaRMM ZIP endpoint returns valid ZIP with install.ps1 and driver files inside a named subfolder
|
|
- .intunewin endpoint returns valid .intunewin (outer ZIP with IntuneWinPackage/ structure)
|
|
- Both endpoints handle 404/422 for missing printer or driver
|
|
- Router registered in main.py
|
|
- All tests pass, full suite still green (pytest tests/ -x)
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
pytest tests/test_packages.py -x && pytest tests/ -x
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- GET /printers/{id}/packages/ninja returns downloadable ZIP with install.ps1 + driver files
|
|
- GET /printers/{id}/packages/intunewin returns downloadable .intunewin package
|
|
- Both endpoints return proper error codes for invalid requests
|
|
- Full test suite green
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/05-package-export/05-01-SUMMARY.md`
|
|
</output>
|