Metadata-Version: 2.4
Name: psyexp-core
Version: 0.5.2
Summary: Task-agnostic harness for PsychoPy experiments: screen/frame-timing setup, run manifests, CSV writers, setup-wizard primitives, instruction pager, and keyboard abstraction.
Project-URL: Homepage, https://github.com/HAPNlab/psyexp-core
Project-URL: Repository, https://github.com/HAPNlab/psyexp-core
Author: Eric Wang
Classifier: Programming Language :: Python :: 3.11
Requires-Python: <3.12,>=3.11
Requires-Dist: prompt-toolkit>=3.0
Requires-Dist: psychopy>=2025.1
Requires-Dist: pyobjc-framework-quartz>=10; sys_platform == 'darwin'
Requires-Dist: questionary>=2.0
Requires-Dist: rich>=14.3.3
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# psyexp-core

[![PyPI](https://img.shields.io/pypi/v/psyexp-core.svg)](https://pypi.org/project/psyexp-core/)
[![Python](https://img.shields.io/pypi/pyversions/psyexp-core.svg)](https://pypi.org/project/psyexp-core/)

Task-agnostic harness for PsychoPy experiments, shared across the lab's task
repos (`heat-task`, `mid-task`, `mid-task-deterministic`). It owns the *plumbing*
that every task duplicates; each task repo keeps only its own stimuli, trial
logic, and record schemas.

## What's in here

| Module | Responsibility |
| --- | --- |
| `screen` | `setup_screen()` — open a fullscreen PsychoPy window, enable VSYNC, run a frame-timing calibration, and return a `ScreenDiagnostics`. |
| `diagnostics` | The `ScreenDiagnostics` dataclass (import-light; no PsychoPy). |
| `rundir` | `make_run_dir(data_dir, label, session_time)` — timestamped output directory. |
| `manifest` | `write_manifest(...)` + `system_info()` — JSON run manifest with system/display/process diagnostics and the resolved `psyexp_core_version`. App-specific fields are injected via `header` / `study_params`. |
| `recording` | `CsvWriter` base class (maps a dataclass record onto a fixed column schema). |
| `wizard` | questionary / prompt_toolkit setup-wizard primitives: shared styles, `ask_text` / `ask_select` / `ask_confirm`, `PosFloatValidator`, `prompt_unique_name`, `quit_app`. |
| `instructions` | `page_through(...)` — a self-paced, keypress-driven instruction pager. |
| `keyboard` | PTB / PsychoPy-event keyboard abstraction: `build_keyboard` / `get_keys` / `wait_for_keys` / `clear_events`, plus the timed-press API for response windows — `get_presses` (name + rt), `reset_clock_on_flip` / `reset_clock` / `clock_time`. |

## Install

`psyexp-core` is published on [PyPI](https://pypi.org/project/psyexp-core/), so
the released harness installs like any other dependency:

```bash
uv add psyexp-core          # or: pip install psyexp-core
```

```toml
# your-task/pyproject.toml
dependencies = ["psyexp-core>=0.5"]
```

Requires Python 3.11+. On macOS the `pyobjc-framework-quartz` dependency is
pulled in automatically for display diagnostics.

## Use from a task repo

You can keep the plain PyPI dependency above, or override the source while
developing. For day-to-day work, point at a local checkout so edits are live
without reinstalling:

```toml
# your-task/pyproject.toml
dependencies = ["psyexp-core"]

[tool.uv.sources]
psyexp-core = { path = "../psyexp-core", editable = true }
```

For a reproducible build pinned ahead of (or instead of) a PyPI release, pin a
tagged git ref instead:

```toml
[tool.uv.sources]
psyexp-core = { git = "ssh://git@github.com/HAPNlab/psyexp-core.git", tag = "v0.5.1" }
```

`write_manifest` records the resolved `psyexp_core_version` so each run is
traceable back to a core version.

### Co-developing core while a task repo keeps the git pin

Lab task repos (e.g. `heat-task`) commit the **git-tag** source above so clones
reproduce exactly, then overlay a local editable install for development:

```bash
uv pip install -e ../psyexp-core
```

**Gotcha:** `uv run` re-syncs the task venv from its `uv.lock` on every launch,
which reverts that editable install straight back to the pinned tag (symptoms:
your local core edits silently don't take effect). Set `UV_NO_SYNC=1` in the task
repo (export it in your shell, or use `uv run --no-sync`) so the editable overlay
sticks; run a manual `uv sync` only when you change other deps, then re-run the
editable install. See heat-task's README ("Co-developing `psyexp-core` locally")
for the full workflow.

## Releasing

Tagging and publishing are deliberately separate, so tags stay cheap to iterate on:

1. **Bump + lock + changelog**, then tag `vX.Y.Z`. The tag runs the checks and
   creates a **draft** GitHub Release — it does **not** publish anything.
2. **Review the draft** Release and publish it. That triggers
   [`publish.yml`](.github/workflows/publish.yml), which uploads to
   [PyPI](https://pypi.org/project/psyexp-core/) via **Trusted Publishing** (OIDC;
   no API token stored).

PyPI versions are immutable, so retagging never republishes; bump the version to
ship new code. See **[docs/releasing.md](docs/releasing.md)** for the full process,
SemVer policy, pre-releases, retag semantics, and the one-time PyPI setup.
