-
Notifications
You must be signed in to change notification settings - Fork 0
T3 API : Python Utils
- T3 API Python Utils
- Authentication helpers
- License utilities
- Collection loading & persistence
- DuckDB / data inspection
- File utilities
- Collection filtering & metadata
- Usage tips
- Next Steps
This is the documentation for t3api-utils Python package.
async def get_authenticated_client_or_error_async() -> T3APIClientInteractive, async entrypoint to obtain an authenticated T3APIClient. Presents a picker for credentials, JWT, or API key auth and returns a ready client.
Returns
-
T3APIClient: an authenticated client.
Raises
-
AuthenticationErroron authentication failure. -
typer.Exitif the user cancels or input is invalid.
Notes
- JWT/API-key flows are handled under the hood. JWT path runs sync creation but returns the client to async code.
def get_authenticated_client_or_error(*, auth_method: Optional[str] = None) -> T3APIClientInteractive, sync wrapper to obtain an authenticated client. If auth_method is omitted, shows a picker (credentials | jwt | api_key).
Params
-
auth_method: optional fixed choice to skip the picker.
Returns
-
T3APIClient.
Raises
-
AuthenticationError,typer.Exit.
Example
client = get_authenticated_client_or_error(auth_method="api_key")def get_jwt_authenticated_client_or_error(*, jwt_token: str) -> T3APIClientCreates a client using a pre-existing JWT. Does not call /whoami automatically.
Params
-
jwt_token: JWT access token.
Returns
-
T3APIClient.
Raises
-
AuthenticationError(invalid token/creation error). -
ValueErrorpropagated asAuthenticationError.
When to use
- You already validated the JWT elsewhere or don’t need a live validation call.
def get_jwt_authenticated_client_or_error_with_validation(*, jwt_token: str) -> T3APIClientSame as above but validates the token by calling /v2/auth/whoami. Closes the client and surfaces actionable messages if invalid/expired/forbidden.
Params
-
jwt_token: JWT access token.
Returns
-
T3APIClient(validated).
Raises
-
AuthenticationErrorwith clear reason (invalid/expired/forbidden). -
ValueErrorwrapped asAuthenticationError.
def get_api_key_authenticated_client_or_error(*, api_key: str, state_code: str) -> T3APIClientAuthenticates using API key + state.
Params
-
api_key: T3 API key. -
state_code: two-letter state code (e.g.,CA,MO,CO,MI).
Returns
-
T3APIClient.
Raises
-
AuthenticationError,ValueErrorwrapped asAuthenticationError.
def pick_license(*, api_client: T3APIClient) -> LicenseDataInteractive license picker. Fetches /v2/licenses, shows a table, and returns the selected license.
Params
-
api_client: authenticated client.
Returns
-
LicenseData: selected license dict.
Raises
-
typer.Exitif none found or selection invalid.
def load_collection(
method: Callable[P, MetrcCollectionResponse],
max_workers: int | None = None,
*args: P.args,
**kwargs: P.kwargs,
) -> List[MetrcObject]Loads all pages of a paginated collection in parallel and returns a flattened list (data items only).
Params
-
method: function that fetches one page and returns aMetrcCollectionResponse. -
max_workers: optional thread pool size. -
*args,**kwargs: forwarded tomethod.
Returns
-
List[MetrcObject]: all items across pages.
Example
items = load_collection(api.list_packages, license_number="CUL000001")def save_collection_to_json(
*,
objects: List[Dict[str, Any]],
output_dir: str = "output",
open_after: bool = False,
filename_override: Optional[str] = None,
) -> PathSerializes a list of dicts to JSON in output/ (or custom dir). The default filename uses {index or 'collection'}__{licenseNumber}__{timestamp}.json.
Params
-
objects: non-empty list of records. -
output_dir: directory to write to. -
open_after: ifTrue, opens the file (platform-dependent). -
filename_override: base name override.
Returns
-
Pathto the saved file.
Raises
-
ValueErrorifobjectsis empty.
def save_collection_to_csv(
*,
objects: List[Dict[str, Any]],
output_dir: str = "output",
open_after: bool = False,
filename_override: Optional[str] = None,
strip_empty_columns: bool = False,
) -> PathSerializes records to CSV with smart field ordering. Supports optional empty-column stripping.
Params
- Same as JSON variant, plus:
-
strip_empty_columns: drop columns that are empty across all rows.
Returns
-
Pathto the saved file.
Raises
-
ValueErrorifobjectsis empty.
def interactive_collection_handler(*, data: List[Dict[str, Any]]) -> NoneMenu-driven TUI workflow for a loaded collection:
- Inspect items
- Filter by CSV matches
- Save to CSV/JSON (custom paths supported)
- Load into DuckDB
- Export DB schema
Params
-
data: list of Metrc-like dicts.
Side effects
- Prompts via
typer, prints viarich. - May open saved files.
- Creates/uses a DuckDB connection for the session.
Notes
- Tracks state (saved paths, DB loaded) and updates the status banner.
def load_db(*, con: duckdb.DuckDBPyConnection, data: List[Dict[str, Any]]) -> NoneLoads nested dictionaries into DuckDB, creating:
- A root table from flattened top-level records.
-
Child tables for nested objects/arrays, named by each nested object’s
data_model. - Deduplicates by (implicit) IDs within extracted tables.
Params
-
con: active DuckDB connection. -
data: structured list of records.
Raises
-
ValueErroron malformed inputs/table creation issues.
Example
con = duckdb.connect()
load_db(con=con, data=records)def inspect_collection(*, data: Sequence[Dict[str, Any]]) -> NoneLaunches a Textual TUI inspector:
- Scrollable JSON with highlighting
- Search with live filtering
- Mouse/keyboard navigation
Params
-
data: list/sequence of dicts.
Notes
- Extracts a friendly collection name for the UI header.
def pick_file(
*,
search_directory: str = ".",
file_extensions: List[str] = [".csv", ".json", ".txt", ".tsv", ".jsonl"],
include_subdirectories: bool = False,
allow_custom_path: bool = True,
load_content: bool = False,
) -> Union[Path, Dict[str, Any]]Interactive file picker that lists recent files (size, modified time, type) and optionally loads content.
Params
-
search_directory: where to look. -
file_extensions: allowed extensions. -
include_subdirectories: recursive search. -
allow_custom_path: add “Enter custom path…” option. -
load_content: ifTrue, returns parsed content.
Returns
- If
load_content=False:Path. - If
True:{"path": Path, "content": Any, "format": str, "size": int}.
Raises
-
typer.Exiton cancellation/no files (when custom path disallowed). -
FileNotFoundErrorif custom path is missing. -
ValueErrorfor unreadable/invalid content.
Supported parsing
-
.json→json.load -
.jsonl/.ndjson→ list of JSON objects -
.csv/.tsv→ list of dict rows - others → raw text
def match_collection_from_csv(
*,
data: List[Dict[str, Any]],
on_no_match: Literal["error", "warn", "skip"] = "warn",
) -> List[Dict[str, Any]]Filters a collection by exact matching against a user-picked CSV/TSV file. CSV headers must exactly equal collection field names.
Params
-
data: collection to filter. -
on_no_match: behavior for unmatched rows:-
"error"→ raise on first miss -
"warn"→ log a warning (default) -
"skip"→ silent skip
-
Returns
- Matched subset (deduplicated, order-preserving). May be empty.
Raises
-
ValueErrorif CSV columns don’t exist in the collection, bad format, or empty. -
typer.Exitif selection is canceled.
CSV example
id,name,status
12345,ProductA,Active
67890,ProductB,Inactive
def extract_collection_metadata(*, data: Sequence[Dict[str, Any]]) -> tuple[str, str]Derives two labels from a collection:
-
Collection name:
"dataModel__index"ifindexis present; otherwise"dataModel". Returns"mixed_datamodels"if multiple values exist;"empty_collection"if empty. -
License number: from
licenseNumber. Returns"mixed_licenses"if multiple values exist;"no_license"if empty input.
Params
-
data: sequence of dicts.
Returns
-
(collection_name, license_number).
Examples
extract_collection_metadata(data=[
{"dataModel": "PACKAGE", "index": "active", "licenseNumber": "CUL00001"},
{"dataModel": "PACKAGE", "index": "active", "licenseNumber": "CUL00001"},
])
# -> ("PACKAGE__active", "CUL00001")
extract_collection_metadata(data=[
{"dataModel": "PACKAGE", "licenseNumber": "CUL00001"},
{"dataModel": "PLANT", "licenseNumber": "CUL00002"},
])
# -> ("mixed_datamodels", "mixed_licenses")- Many functions are interactive and designed for CLIs built on
typer+rich. - When scripting, prefer the non-interactive constructors in your own code paths (e.g., pass
auth_methodor use the explicit JWT/API key helpers). - For large collections,
load_collectionparallelizes pagination automatically; adjustmax_workersfor your environment. - Use
load_db→duckdbfor quick local analytics andexport_duckdb_schema(via the interactive handler) to understand the generated schema.
- Refer to the T3 API documentation to explore all the available endpoints.
- Most API endpoints require a T3+ subscription. If you don't have a T3+ subscription, you can sign up here.
Created by Matt Frisbie
Contact: matt@trackandtracetools
Copyright © 2025 Track & Trace Tools. All rights reserved.
- Home
- FAQ
- Metrc
- T3 Chrome Extension
- T3 API
- OpenTag
- T3 API : Python Utils
- T3 Labels : Label Templates
- T3 Chrome Extension : Exports
- T3 Chrome Extension : Scan Sheets
- T3 Labels : Tutorial
- RFID
- T3 Chrome Extension : CSV Form Fill
- T3 Labels : Label Layouts
- T3+
- T3 API : Setting Up Python
- T3 Chrome Extension : T3+ Features
- T3 Labels : Printing Label PDFs
- T3 API : API Scripts
- T3 Chrome Extension : Label Studio
- T3 Chrome Extension : Primary Features
- T3 Chrome Extension : Getting Started
- T3 Labels
- T3 Labels : Generating Label PDFs
- T3 API : Reports and Spreadsheet Sync
- T3 API : Getting Started
- T3 Labels : Getting Started