This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Python client for the Frameshift Mosaic REST API, plus ~200 standalone CLI scripts that use it.
Two layers:
mosaic.py(~2300 lines) — the entire client library. One file, three classes.- Topic directories (
projects/,samples/,variant_annotations/,views/, …) — thin, single-purpose CLI wrappers around client methods. Directory names mirror Mosaic API resource groups; file names mirror the HTTP verb + route (projects/post_project.py,views/put_update_view.py).
There is no build, no test suite, no linter, and no packaging. Scripts are run directly.
pip3 install requests # only hard dependencyA .venv/ (Python 3.13) exists locally. *.ini is gitignored — config files hold live API tokens and must never be committed.
config.ini:
[Configuration]
token = <generated in the Mosaic UI>
host = http://localhost:3000/api/v1
[Project ids]
annotations_grch37 = 1
annotations_grch38 = 2
Every script takes the same two API arguments:
python3 projects/get_projects.py -c config.ini
python3 project_attributes/put_project_attributes.py -c config.ini -p 123 -i 456 --name "New name"--client_config/-c(required) — path to the ini file. A few older scripts call this--config/-c.--api_client/-a(optional) — path to this repo. Every script now locates it from its own position, so the flag is only needed to point at a different checkout. The checkout directory no longer has to be namedapi_client; it used to, because scripts derived the path by splitting their own realpath on that literal string.--project_id/-p— required by any script that operates on a project.
Interactive/interpreter use:
from mosaic import Mosaic
api_mosaic = Mosaic(config_file='config.ini')
project = api_mosaic.get_project(123)
for attribute in project.get_project_attributes():
print(attribute['name'])mosaic.py also has a python-fire entrypoint (python mosaic.py project --project-id=1 <method>), but fire is not installed in .venv and this path is rarely used.
Store — thin configparser wrapper over the ini file; get/set (set writes back to disk).
Mosaic — auth headers, HTTP verbs, and all non-project-scoped routes (get_projects, get_hpo_terms, get_user_by_email, post_policies, whitelist/super-admin, role types, data resources…).
_http_requestis the single choke point: coerces list query params tokey[],json.dumpsbodies to avoid form encoding, handles multipart whenfile_uploadis passed (clearsContent-Typesorequestssets the boundary), and re-raises non-2xx asHTTPErrorcarrying the server'smessagefield.- Every request is recorded as a replayable
curlstring in_request_history(request_history()). get_paged_route_iter(resource, params=...)— generator that walkspage/limituntilcountis satisfied. Any list endpoint should be exposed withyield from get_paged_route_iter(...), not a bareget(); ~17 methods already do this, and callers iterate rather than index.
Project — everything under projects/{id}. Holds a reference to its backing Mosaic (self._mosaic) and a path prefix (self._path = f"projects/{self.id}"); all methods build routes from that prefix. Constructed via api_mosaic.get_project(project_id), which fetches the project first so .name and .data are populated.
Collections are just projects with is_collection true and a collection_project_ids list — scripts that must work on both typically fan out over that list (see project_setup/set_project_defaults.py).
Add a method to Mosaic or Project (whichever the route is scoped to), placed under the existing """ SECTION """ comment banner for that resource group, alphabetically within the section. House style for the method body:
def put_something(self, thing_id, *, name=None, description=None):
data = { }
if name:
data['name'] = name
if description:
data['description'] = description
return self._mosaic.put(f'{self._path}/things/{thing_id}', data=data)Required values are positional; everything optional is keyword-only and omitted from the payload when falsy.
Every script starts up through _bootstrap.py. There is no second pattern; if you find one deriving its own path or defining its own fail(), it predates this and should be brought in line.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
from _bootstrap import base_parser, init, fail
def parse_command_line():
parser, groups = base_parser()
groups.project.add_argument('--project_id', '-p', required = True, metavar = 'integer', help = 'The Mosaic project id')
return parser.parse_args()
def main():
args = parse_command_line()
api_mosaic = init(args)base_parser()returns(parser, groups).groupsis aSimpleNamespacewith.api .project .required .optional .display, pre-titled to match the legacy group names, and already carrying--client_configand--api_client. A script needing its own group adds it to the returnedparser.init(args)returns onlyapi_mosaic. It does not return aStore— the old preamble built one in nearly every script and not one of them ever called a method on it. Read config viaapi_mosaic.get_config(section, key), asvariant_annotations/upload_annotations.pydoes.fail/warningcome from_bootstrap; don't redefine them.- Because the repo root goes on the front of
sys.pathfor every script, never add an__init__.pyto a topic directory, and never add a root-level file whose name shadows a stdlib module. project_attributes/get_project_attributes.pyis the reference migration; copy its shape.- Scripts that declare no groups of their own keep their arguments on the parser and discard the handle:
parser, _ = base_parser().
- 2-space indentation in scripts and
_bootstrap.py; 4-space inmosaic.py. argparseuses the group names above and spaces around=in kwargs (required = False). Roughly a third of the scripts declare their arguments straight on the parser instead.- Output goes to stdout via
print/pprint; most scripts offer--raw_output/-roor--display_all/-dafor a full dump. Scripts do not return values, do not prompt for confirmation (including thedelete_all_*ones), and validate enum-ish inputs against a localallowed_*list before calling the API. --ids_only/-iois the convention for machine-readable output meant to be piped into another script.- The config flag is
--client_config/-ceverywhere. Nine scripts used to spell the long form--config; that spelling no longer works.
project_setup/— multi-step provisioning scripts driven by JSON config (project_defaults_GRCh37.json,project_defaults_GRCh38.json,annotations.json) that map attribute UIDs to sample-table columns, charts, and variant filters.custom_scripts/andmaintenance_scripts/— one-off reporting and data-cleanup jobs (UDN/NIH-specific), not general API wrappers.super_admin/— routes requiring elevated tokens.