-
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() -> T3APIClient
Interactive, 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
-
AuthenticationError
on authentication failure. -
typer.Exit
if 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) -> T3APIClient
Interactive, 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) -> T3APIClient
Creates 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). -
ValueError
propagated 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) -> T3APIClient
Same 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
-
AuthenticationError
with clear reason (invalid/expired/forbidden). -
ValueError
wrapped asAuthenticationError
.
def get_api_key_authenticated_client_or_error(*, api_key: str, state_code: str) -> T3APIClient
Authenticates 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
,ValueError
wrapped asAuthenticationError
.
def pick_license(*, api_client: T3APIClient) -> LicenseData
Interactive 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.Exit
if 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,
) -> Path
Serializes 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
-
Path
to the saved file.
Raises
-
ValueError
ifobjects
is 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,
) -> Path
Serializes 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
-
Path
to the saved file.
Raises
-
ValueError
ifobjects
is empty.
def interactive_collection_handler(*, data: List[Dict[str, Any]]) -> None
Menu-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]]) -> None
Loads 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
-
ValueError
on malformed inputs/table creation issues.
Example
con = duckdb.connect()
load_db(con=con, data=records)
def inspect_collection(*, data: Sequence[Dict[str, Any]]) -> None
Launches 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.Exit
on cancellation/no files (when custom path disallowed). -
FileNotFoundError
if custom path is missing. -
ValueError
for 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
-
ValueError
if CSV columns don’t exist in the collection, bad format, or empty. -
typer.Exit
if 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"
ifindex
is 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_method
or use the explicit JWT/API key helpers). - For large collections,
load_collection
parallelizes pagination automatically; adjustmax_workers
for your environment. - Use
load_db
→duckdb
for 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