Metadata-Version: 2.5
Name: visualkit
Version: 0.1.0
Summary: Read-only access to the Infor VISUAL Manufacturing 10 ERP database (SQL Server)
Project-URL: Source, https://gt.pescoinc.biz/spencerm/VisualKit
Author-email: Spencer McGuire <spencerm@pescoinc.biz>
License: Proprietary - PESCO internal use
Classifier: Private :: Do Not Upload
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pydantic>=2.7
Requires-Dist: pyodbc>=5.1
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: twine>=6; extra == 'dev'
Provides-Extra: keyring
Requires-Dist: keyring>=25; extra == 'keyring'
Description-Content-Type: text/markdown

# VisualKit

Read-only Python access to PESCO's Infor VISUAL Manufacturing 10 ERP database (SQL Server). See [PLAN.md](PLAN.md) for the design and roadmap, and [CHANGELOG.md](CHANGELOG.md) for releases.

**Using VisualKit in your own app or scheduled job? Start with [docs/INSTALL.md](docs/INSTALL.md).** This README is for working on VisualKit itself.

VisualKit never changes anything in VISUAL. Only `SELECT` statements get through, and it connects to DEV unless production is chosen explicitly.

## Install (development)

You need Python 3.11+ and Microsoft ODBC Driver 18 for SQL Server (64-bit). On Linux, see [docs/INSTALL.md](docs/INSTALL.md#linux) for the driver.

```bash
python -m venv .venv
.venv\Scripts\python -m pip install -e ".[dev,keyring]"
.venv\Scripts\visualkit doctor          # checks settings, driver, encryption, auth, connectivity and that it can't write
```

On Linux, use `.venv/bin/` instead of `.venv\Scripts\`, and leave out `keyring`.

## Configure

Copy `.env.example` to `.env`. The defaults point at DEV (`VSQL\DEV` / `PESCOINCDEV`). Don't put the password in `.env`.

- **Windows, before `visualkit_ro` exists:** leave `VISUAL_USER` unset. VisualKit uses your Windows login (integrated auth). Keep to DEV and small queries.
- **With `visualkit_ro`:** set `VISUAL_USER=visualkit_ro`, then either
  - store the password once in Windows Credential Manager: `.venv\Scripts\keyring set visualkit visualkit_ro`, or
  - set `VISUAL_PASSWORD`, or `VISUAL_PASSWORD_FILE` pointing at a file that holds only the password.

Production (`vsql` / `PESCOINC`) is only used with `VISUAL_PROFILE=prod`, or with `VISUAL_SERVER=vsql` and `VISUAL_DATABASE=PESCOINC` both set. All settings are listed in [docs/INSTALL.md](docs/INSTALL.md#settings).

## Use

```python
from visualkit import Visual

with Visual.from_env() as v:
    print(v.server_info())                     # SQL Server version, database, login, app name, VISUAL DBVERSION
    for site in v.sites():
        print(site)

    rows = v.query("SELECT ID, DESCRIPTION FROM PART WHERE ID LIKE ?", ["L073%"], max_rows=100)
    count = v.scalar("SELECT COUNT(*) FROM PART")
    for row in v.stream("SELECT ID FROM PART"):  # fetched in batches of VISUAL_BATCH_SIZE
        ...
```

- `query()` stops at `VISUAL_MAX_ROWS` rows (10,000) unless `max_rows=` says otherwise. Use `stream()` for big reads.
- Every query has a timeout (`VISUAL_TIMEOUT`, 60 s).
- Anything that isn't a single `SELECT` is refused before it reaches the server.
- ID columns come back as text. Keep them that way.

### Domains

Five typed, read-only repositories hang off `Visual`. They return frozen pydantic models. The full reference is in [docs/INSTALL.md](docs/INSTALL.md#api-reference).

```python
from datetime import date
from visualkit import Visual, WorkOrderKey

with Visual.from_env() as v:
    # Parts
    part = v.parts.get("L073-76-2")
    parts = v.parts.get_many(["L073-76-2", "L073-76-2GA"])         # {id: Part}
    planning = v.parts.site("L073-76-2GA", "PESCO")                  # PART_SITE: price, primary location

    # Customer orders
    order, customer = v.orders.with_customer("51049")
    lines = v.orders.lines("51049")
    open_orders = v.orders.list(open_only=True, limit=50)            # a Page

    # Work orders
    wo = v.work_orders.get(WorkOrderKey.parse("L073-75-1/53/0/0"))
    ops = v.work_orders.operations(wo.key)                           # op.barcode() for Packet Station
    feed = v.work_orders.released("PESCO", date(2026, 10, 1), date(2026, 10, 31))

    # Inventory
    stock = v.inventory.on_hand("L073-76-2GA", site_id="PESCO")      # nonzero locations

    # Shipments
    packlists = v.shipments.for_order("51049")
    shipped = v.shipments.for_order_line("51049", 1)                 # [] means not shipped yet
```

List calls return a `Page`: `page.items`, `page.has_more`, `page.next_offset`.

### CLI

```bash
visualkit doctor | info | sites
visualkit parts get L073-76-2 L073-76-2GA          # also: list, search, site
visualkit orders get 51049                         # also: list, lines
visualkit wo get L073-75-1/53/0/0                  # or a bare base ID: lot 1, split 0, sub 0
visualkit wo released --since 2026-10-01 --until 2026-11-14 --limit 5
visualkit wo barcode "*%2L073-75-1$53$0$0$10%*" --fetch
visualkit inventory onhand L073-76-2GA             # also: list, warehouses, locations
visualkit ship get 77881                           # also: list, for-order, for-line
```

Every data subcommand prints a table, or JSON with `--json`. Most take `--site`. List subcommands take `--limit` and `--offset`; when there are more rows, the next `--offset` goes to stderr. `--width` sets where long text is cut (40 by default). Dates are `YYYY-MM-DD` or ISO. Run `visualkit <domain> <action> --help` for the filters.

Exit codes: 0 fine, 1 not found or a VISUAL error, 2 bad settings or input. Production needs `--allow-production`.

### Packet Station example

[examples/packet_station_feed.py](examples/packet_station_feed.py) replaces the two Power BI feeds Packet Station reads today. It writes one JSON object with `meta`, `rows` (Job Release Information: one row per operation, with `wo, lot, split, sub, seq, res, dwg, rev, ps, desc, qty, tgt, rel, want, st`, plus `barcode`, `part`, `site` and `order`), `materials` (Job Pull: one row per material line, with `wo, lot, split, sub, seq, piece, part, qty, issued, whs, loc, st, istat, dwg, desc, comm, cons`), `orders_by_base` and `orders_by_project`.

Customer orders come from DEMAND_SUPPLY_LINK (`v.links`), not from part-number prefixes. `orders_by_base` holds the orders a base is pegged to directly; only whole-number top jobs (L489001) and field-service jobs are. `orders_by_project` holds the orders of each project's top jobs, found by PESCO's "project plus three digits" naming (L489 -> L489001); that is a naming convention, not a VISUAL relationship. Each row's `order` is the base's own order, else the project's first open order, else its newest order, else null, with `via` saying which. Order entries carry the order ID, status, customer ID and name, customer PO and dates; no amounts.

```bash
.venv\Scripts\python examples\packet_station_feed.py --since 2026-09-30 --out feed.json
```

- `--since` is required. `--until` is optional; without it there is no upper bound, like the Power BI report.
- `--status` picks the work order statuses for rows (default `F,R,U,C`). `--materials-status` picks the bases and legs for materials (default `F,R`).
- `--no-materials` leaves materials out. `--no-orders` leaves out `orders_by_base`, `orders_by_project` and `order`. `--full` adds the raw `work_orders`, `operations`, `requirements`, `parts` and `orders` sections; `orders` lists the directly linked orders with their bases and lines.
- PARTSPECS (`ps`) comes from OPERATION_BINARY; the text of material lines with no part comes from REQUIREMENT_BINARY.
- Numbers are JSON numbers, dates `YYYY-MM-DD`, IDs strings. No cost, price or pay field is written.

On DEV, since 2026-09-30 with no upper bound: 1,533 work orders, 5,962 rows and 16,956 material lines in about 7.5 seconds. Compared with the Power BI export of 2026-10-14, every matched field agrees except where DEV changed after the export (see PLAN.md section 8b).

## Test

```bash
.venv\Scripts\python -m pytest
```

Tests use a fake cursor and need no database. Integration tests run against DEV only, when asked:

```powershell
$env:VISUALKIT_INTEGRATION = "1"; .venv\Scripts\python -m pytest
```

## Release

1. Bump `__version__` in `src/visualkit/__init__.py` and add a `CHANGELOG.md` entry.
2. Run the tests, then build:
   ```bash
   .venv\Scripts\python -m pytest
   .venv\Scripts\python -m build
   ```
3. Upload to the Gitea package registry. This needs a Gitea access token with `write:package` scope; paste it when twine asks for the password:
   ```bash
   .venv\Scripts\python -m twine upload --repository-url https://gt.pescoinc.biz/api/packages/spencerm/pypi -u spencerm dist\*
   ```
4. If any runtime dependency version changed, update `requirements/runtime-lock.txt`, then mirror the dependencies to the registry for every platform and Python version, so installs keep working from PESCO's registry alone. `pyodbc` has a separate wheel per platform and Python version, so this step matters.
   - Resolving for another platform needs [uv](https://docs.astral.sh/uv/), because pip applies `sys_platform` conditions using the machine it runs on.
   - Repeat the steps below for `x86_64-pc-windows-msvc` with `--platform win_amd64`, and for `x86_64-manylinux_2_17` with `--platform manylinux2014_x86_64`.
   - Repeat for each Python version 3.11 to 3.14.
   ```bash
   uv pip compile requirements.in -c requirements\runtime-lock.txt --find-links dist --python-platform x86_64-manylinux_2_17 --python-version 3.11 -o resolved.txt
   .venv\Scripts\python -m pip download -r resolved.txt --no-deps --only-binary=:all: --platform manylinux2014_x86_64 --implementation cp --python-version 3.11 -d wheelhouse
   .venv\Scripts\python -m twine upload --skip-existing --repository-url https://gt.pescoinc.biz/api/packages/spencerm/pypi -u spencerm wheelhouse\*.whl
   ```
   Here `requirements.in` holds the single line `visualkit==<version>`. Remove that line from `resolved.txt` before downloading, since the package itself is uploaded from `dist`. For new dependency releases, pick versions at least 14 days old.
5. Tag the release: `git tag v0.1.0` and `git push --tags`.

Source: `https://gt.pescoinc.biz/spencerm/VisualKit`. Proprietary, PESCO internal.
