fix: resolve test failures and update flake.nix for factur-x source distribution
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -664,3 +664,149 @@ async def http_exception_handler(request: Request, exc: HTTPException):
|
||||
- Run container: `docker run -d --name test -p 5000:5000 zugferd-service:test`
|
||||
- Test health: Use internal curl or Python when host port forwarding problematic
|
||||
|
||||
|
||||
## [2026-02-04T21:50:00.000Z] Task 16: Nix Flake Packaging
|
||||
|
||||
### flake.nix Structure
|
||||
- Uses `buildPythonApplication` for zugferd-service (not buildPythonPackage)
|
||||
- Python 3.11 base via `python311Packages`
|
||||
- `pyproject = true` for hatchling-based builds
|
||||
- `pythonRelaxDeps = true` for dependency flexibility (important for factur-x)
|
||||
- Outputs: `packages.default` and `packages.zugferd-service` both point to same derivation
|
||||
- devShell includes all development dependencies (pytest, pytest-asyncio, httpx)
|
||||
|
||||
### factur-x Package Handling
|
||||
- **NOT available in nixpkgs** - must package inline
|
||||
- Package name on PyPI is `factur_x` (underscore), not `factur-x` (hyphen)
|
||||
- Current version: 3.8 (not 2.5 as in pyproject.toml)
|
||||
- Format: wheel (not source tarball) - must specify `format = "wheel"`
|
||||
- Hash calculation: Use Python to calculate base64 SHA256 hash:
|
||||
```python
|
||||
import base64, hashlib
|
||||
print(base64.b64encode(hashlib.sha256(open('file.whl','rb').read()).digest()).decode())
|
||||
```
|
||||
- Dependencies: lxml, pypdf>=5.3.0
|
||||
- Hash format: `sha256-alctEgMZw79S2UStnt/bYTigE6h9wqCVpm7i1qc5efs=` (base64 encoded)
|
||||
|
||||
### fetchPypi Hash Format
|
||||
- nix-prefetch-url outputs 39-character base64 hash (not SRI format)
|
||||
- Nix expects hash in format: `sha256-<base64-hash>`
|
||||
- Example: `sha256-alctEgMZw79S2UStnt/bYTigE6h9wqCVpm7i1qc5efs=`
|
||||
- Invalid format example (from nix-prefetch-url output): `1yvr76kxdqkflsas1hkxm09s0f31vggrxba4v59bzhqr0c92smva` (wrong length)
|
||||
|
||||
### Git Tracking Requirement for Nix
|
||||
- flake.nix must be added to git (`git add flake.nix`)
|
||||
- Nix requires files to be tracked by git to see them in evaluation
|
||||
- Running `nix flake check` will fail if flake.nix is not tracked
|
||||
- flake.lock is auto-generated on first flake check
|
||||
|
||||
### nix flake check Verification
|
||||
- Validates syntax and evaluates all derivations
|
||||
- Checks packages.default and packages.zugferd-service
|
||||
- Checks devShells.default
|
||||
- Outputs derivation paths (e.g., `/nix/store/...-zugferd-service-1.0.0.drv`)
|
||||
- Syntax valid even if full build not run
|
||||
|
||||
### Inline Python Package Pattern
|
||||
```nix
|
||||
factur-x = pythonPackages.buildPythonPackage rec {
|
||||
pname = "factur_x"; # PyPI name (may differ from import name)
|
||||
version = "3.8";
|
||||
format = "wheel"; # or "pyproject" or "setuptools"
|
||||
|
||||
src = pythonPackages.fetchPypi {
|
||||
inherit pname version format;
|
||||
hash = "sha256-alctEgMZw79S2UStnt/bYTigE6h9wqCVpm7i1qc5efs=";
|
||||
};
|
||||
|
||||
dependencies = with pythonPackages; [ pypdf lxml ];
|
||||
pythonRelaxDeps = true; # Relax exact version constraints
|
||||
|
||||
meta = {
|
||||
description = "Python library to generate and read Factur-X invoices";
|
||||
license = pkgs.lib.licenses.mit;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Dependencies in buildPythonApplication
|
||||
- `dependencies`: Runtime dependencies (fastapi, uvicorn, pydantic, etc.)
|
||||
- `nativeCheckInputs`: Test dependencies (pytestCheckHook, pytest-asyncio, httpx)
|
||||
- `build-system`: Build-time dependencies ([pythonPackages.hatchling])
|
||||
|
||||
### passthru.mainProgram
|
||||
- Sets the main program name for `nix run`
|
||||
- Value: `mainProgram = "zugferd-service"` (matches pyproject.toml [project.scripts])
|
||||
- Allows `nix run .#zugferd-service` to start the service
|
||||
|
||||
### flake-utils Usage
|
||||
- `flake-utils.lib.eachDefaultSystem` applies config to all systems
|
||||
- Access pkgs via `pkgs = nixpkgs.legacyPackages.${system}`
|
||||
- Python packages via `pythonPackages = pkgs.python311Packages`
|
||||
|
||||
|
||||
## [2026-02-04T21:55:00.000Z] Task 17: NixOS Service Module Example
|
||||
|
||||
### NixOS Module Pattern
|
||||
- Standard module structure: `{ config, lib, pkgs, ... }: with lib; let cfg = ...; in { options = ...; config = ...; }`
|
||||
- Service options nested under `services.<service-name>`
|
||||
- Use `mkEnableOption` for boolean enable flags
|
||||
- Use `mkOption` with types for configuration values
|
||||
|
||||
### Service Configuration Options
|
||||
- `enable`: `mkEnableOption "description"` - boolean toggle
|
||||
- `port`: `types.port` - auto-validates 1-65535 range
|
||||
- `host`: `types.str` - string type
|
||||
- `package`: `types.package` - Nix package type with default from pkgs
|
||||
|
||||
### systemd Service Configuration
|
||||
- Service name matches option name: `systemd.services.zugferd-service`
|
||||
- `wantedBy`: `[ "multi-user.target" ]` - starts on system boot
|
||||
- `after`: `[ "network.target" ]` - starts after network is ready
|
||||
- `serviceConfig` keys:
|
||||
- `Type = "simple"` - standard long-running service
|
||||
- `ExecStart` - command to run service
|
||||
- `Restart = "on-failure"` - restart on crashes
|
||||
- `DynamicUser = true` - creates unprivileged user automatically
|
||||
- `NoNewPrivileges = true` - security hardening
|
||||
- `ProtectSystem = "strict"` - filesystem protection
|
||||
- `ProtectHome = true` - home directory protection
|
||||
|
||||
### ExecStart Pattern
|
||||
- Must convert port to string with `toString cfg.port`
|
||||
- String interpolation: `${cfg.package}/bin/zugferd-service --host ${cfg.host} --port ${toString cfg.port}`
|
||||
- Entry point from pyproject.toml: `zugferd-service = "src.main:run"` generates `/bin/zugferd-service`
|
||||
- run() function accepts host and port arguments, passed via CLI flags
|
||||
|
||||
### Module Verification
|
||||
- Use `nix-instantiate --parse module.nix` to verify Nix syntax
|
||||
- Parses successfully = valid syntax
|
||||
- Check file exists: `ls -la nix/module.nix`
|
||||
|
||||
### NixOS Module Usage Example
|
||||
```nix
|
||||
# configuration.nix
|
||||
{
|
||||
imports = [ /path/to/zugferd-service/nix/module.nix ];
|
||||
|
||||
services.zugferd-service = {
|
||||
enable = true;
|
||||
port = 5000;
|
||||
host = "127.0.0.1";
|
||||
package = pkgs.zugferd-service;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Example Module Limitations
|
||||
- This is an example module, not production-ready
|
||||
- No authentication or TLS configuration (open endpoints per spec)
|
||||
- Minimal configuration options (can be extended for production use)
|
||||
- Service is stateless (no database or persistent storage needed)
|
||||
|
||||
### NixOS Module Best Practices
|
||||
- Use `mkIf cfg.enable` to only apply config when service is enabled
|
||||
- Default values should match application defaults (5000, 127.0.0.1)
|
||||
- Package option allows override for testing different versions
|
||||
- Security hardening options (DynamicUser, NoNewPrivileges, ProtectSystem) standard practice
|
||||
|
||||
|
||||
Reference in New Issue
Block a user