Metadata-Version: 2.5
Name: mfileskit
Version: 0.1.3
Summary: Read-only access to M-Files vault metadata and files over the M-Files Web Service (REST) API
Project-URL: Source, https://gt.pescoinc.biz/spencerm/MFilesKit
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: httpx>=0.27
Requires-Dist: keyring>=25
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pydantic>=2.7
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: twine>=6; extra == 'dev'
Provides-Extra: keyring
Description-Content-Type: text/markdown

# MFilesKit

Read-only Python access to PESCO's M-Files vault (metadata and files) over the M-Files Web Service REST API. See [PLAN.md](PLAN.md) for the design and roadmap, and [CHANGELOG.md](CHANGELOG.md) for releases.

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

## Install (development)

```bash
python -m venv .venv
.venv\Scripts\python -m pip install -e ".[dev]"
.venv\Scripts\mfileskit doctor          # checks settings, password, server, sign-in and permissions
```

## Configure

Copy `.env.example` to `.env` and fill in `MFILES_URL`. Don't put the password in `.env`. Either:

- store it once in Windows Credential Manager: `.venv\Scripts\keyring set mfileskit mfileskitRO`, or
- set `MFILES_PASSWORD` in the environment of the process that runs the library.

The CLI prompts for it if neither is set.

## Use

```python
from mfileskit import Vault

with Vault.from_env() as vault:
    print(vault.info.name)
    drawing = vault.schema.cls("Drawing (DWG)")
    rev = vault.schema.property("Revision number", cls=drawing.id)
```

```bash
mfileskit info                         # sign in, show vault and schema counts
mfileskit schema                       # list object types
mfileskit schema --type Document       # classes of an object type
mfileskit schema --class "Drawing (DWG)"   # properties of a class
mfileskit schema --find drawing        # search class and property names
```

### Drawings (Packet Station's lookup)

```bash
mfileskit drawings status L073-76-2 K978-01-6H13         # released rev, draft rev, drafting status
mfileskit drawings status --in data\feed\want.json       # batch, JSON keyed by want.json item id
mfileskit drawings fetch L073-76-2 --rev 3 --out x.pdf   # download only if the released rev is 3
```

```python
from mfileskit.drawings import DrawingLookup

with Vault.from_env() as vault:
    lookup = DrawingLookup(vault)
    st = lookup.status("L073-76-2")
    if lookup.matches(st, visual_rev="3").attach:
        lookup.download(st, "L073762X r3.pdf")
```

See [docs/packet-station.md](docs/packet-station.md) for how drawings are laid out in the vault.

### Bulk export and sync

```bash
mfileskit count --class "Drawing Production (PDF)"                        # list only, no downloads
mfileskit export --class "Drawing Production (PDF)" --ext pdf --out D:\exports\drawings
mfileskit export --class "Drawing Production (PDF)" --ext pdf --out D:\exports\drawings              # again: only changes
mfileskit export --class "Drawing Production (PDF)" --ext pdf --out D:\exports\drawings --reconcile  # also record deletions
```

Current drawings only, kept as a mirror that drops replaced revs:

```bash
mfileskit export --class "Drawing Production (PDF)" --ext pdf --current --prune --out D:\exports\current-drawings
```

- `--current` is shorthand for `--where "Controlled Status=Effective"`. `--where "PROPERTY=VALUE|VALUE"` works with any property's display text, and can be repeated.
- The filter is applied after listing the whole class. That way, when a drawing becomes Superseded, the export notices: it writes a `deleted` line with the reason, and `--prune` deletes its files.
- `--prune` sweeps the whole folder on every run. It keeps only each object's latest exported version, and deletes the files of removed objects. Running it once on a folder that was exported without `--prune` tidies that folder too. It only ever deletes version folders (`files/<type>/<id>/<version>/`) inside the export folder, never anything in M-Files. A folder that can't be deleted (a file open in a viewer) is logged and retried on the next run.

Output, one job per folder:

- `files/<type>/<id>/<version>/<file>`
- `manifest.jsonl`: one line per exported object version, with all properties (name, display text, and the raw value or lookup IDs) and its files (path, size, SHA-256). Deletions are added as `{"deleted": true, "reason": ...}` lines. When reading, keep the last line per `(type, id)`.
- `.mfileskit/state.sqlite`: what was exported, plus the checkpoint and the job's scope (classes, type, filters, `--ext`, `--no-files`). A run with a different scope is refused: use a new folder.
- `.mfileskit/lock.sqlite`: held while a run is in progress, so a second run on the folder fails at once.
- `.mfileskit/runs.jsonl`: a summary of each run.

Searches stop at 500 results. The export gets around this by splitting the query into "Last modified" time windows, and it checks every result against the window it asked for. The checkpoint only advances after a complete run with no failures, so if a run fails, just run it again.

Names are matched case-insensitively. A few property names are reused in the vault (e.g. "Description"). Pass `cls=` to narrow them, or use the ID or GUID.

## Release

1. Bump `__version__` in `src/mfileskit/__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`. The first section lists the versions the tests ran with. The second section has dependencies that only apply to Python 3.11 or Linux; for those, pick releases that are at least 14 days old. Then mirror them to the registry for every platform and Python version, so installs keep working from PESCO's registry alone.
   - 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–3.14.
   ```bash
   echo mfileskit==<version> > requirements.in
   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
   ```
   `requirements.in` is a scratch file (not committed) holding the single line `mfileskit==<version>`. Remove that line from `resolved.txt` before downloading, since the package itself is uploaded from `dist`.
5. Tag the commit you built from: `git tag v<version>` and `git push --tags`. The registry won't take a second upload of the same version, so any fix after uploading needs a new version number.

## Test

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