Skip to content

zyfy-uk/zyfy-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zyfy

PyPI version Python versions License: MIT

Official Python client library for the Zyfy UK data enrichment API.

Two products in one package:

  • Vehicle Intelligence — DVLA + DVSA MOT history, ULEZ, tax status, odometer trend, buy recommendation
  • Postcode Intelligence — broadband, flood risk, crime, property prices, deprivation, air quality, MP data, and more

Get your API key at zyfy.uk/signup. Full API docs at zyfy.uk/docs.

Installation

pip install zyfy

Requires Python 3.9 or later.

Quickstart

Synchronous

from zyfy import Zyfy

client = Zyfy(api_key="ea_live_...")

# Vehicle lookup
vehicle = client.vehicle.lookup("AB12CDE")
print(vehicle.summary.buy_recommendation)  # "good" | "consider" | "caution" | "avoid"
print(vehicle.signals.mot_expiry_date)
print(vehicle.quota.remaining)             # requests left this month

# Postcode lookup
postcode = client.postcode.lookup("SW1A 2AA")
print(postcode.summary.liveability_level)  # "low" | "medium" | "high"
print(postcode.signals.crime.rate_band)
print(postcode.quota.remaining)

Asynchronous

import asyncio
from zyfy import AsyncZyfy

async def main() -> None:
    async with AsyncZyfy(api_key="ea_live_...") as client:
        vehicle = await client.vehicle.lookup("AB12CDE")
        print(vehicle.summary.buy_recommendation)

        postcode = await client.postcode.lookup("SW1A 2AA")
        print(postcode.signals.crime.rate_band)

asyncio.run(main())

The API key can also be set via the ZYFY_API_KEY environment variable — if it is, you can construct the client with no arguments:

client = Zyfy()               # reads ZYFY_API_KEY
client = AsyncZyfy()          # same

Both clients support context managers for automatic cleanup:

with Zyfy() as client:
    result = client.vehicle.lookup("AB12CDE")

async with AsyncZyfy() as client:
    result = await client.vehicle.lookup("AB12CDE")

Configuration reference

All options are keyword-only. All are optional if ZYFY_API_KEY is set.

Option Type Default Description
api_key str ZYFY_API_KEY env var Your Zyfy API key.
max_enrichment_retries int 10 Auto-retries when enrichment_pending=True. Each retry waits the Retry-After seconds from the response. Set to 0 to disable.
timeout_ms int 10000 Per-request timeout in milliseconds. Surfaces as NetworkError on expiry.
base_url str https://zyfy.uk/v1 Override for local development or testing.
debug bool False Logs requests and responses via the zyfy logger at DEBUG level. The API key is always redacted.

Full reference

client.vehicle.lookup(registration, *, max_enrichment_retries=None)

Look up a single UK vehicle by registration mark. Returns DVLA details, DVSA MOT history, emissions, and computed intelligence.

Full signal reference: zyfy.uk/docs/vehicle.html

Parameter Type Description
registration str UK registration mark. Spaces and case are normalised automatically.
max_enrichment_retries int | None Per-call override of max_enrichment_retries.

Returns: VehicleResult


client.vehicle.bulk_lookup(registrations, *, max_enrichment_retries=None)

Synchronous bulk vehicle lookup. Runs sequentially on the server. Up to your tier's bulk cap per call.

Returns: BulkVehicleResult

Use is_vehicle_bulk_error(item) to distinguish error items from successful results:

from zyfy import Zyfy, is_vehicle_bulk_error

result = client.vehicle.bulk_lookup(["AB12CDE", "NOTREAL"])
for item in result.results:
    if is_vehicle_bulk_error(item):
        print(item.registration, item.error)  # "not_found" | "invalid_format"
    else:
        print(item.registration, item.summary.buy_recommendation if item.summary else None)

client.vehicle.submit_bulk(registrations)

Submit an async bulk job. Returns immediately with a job_id. Poll with get_job().

Returns: BulkJobSubmitted


client.vehicle.get_job(job_id)

Poll the status of an async bulk job. Check status == "complete" before reading results.

Returns: BulkJobStatus[VehicleResult | VehicleBulkItemError]

status values: "queued" | "processing" | "complete" | "expired"


client.vehicle.delete_job(job_id)

Delete a bulk job and its results. Jobs expire automatically after 24 hours.

Returns: DeletedJob


client.postcode.lookup(postcode, *, max_enrichment_retries=None)

Look up a single UK postcode. Returns geographic classification, broadband, flood risk, property prices, crime, deprivation, air quality, housing, political, and demographic signals.

Northern Ireland postcodes (BT prefix) are not supported. Raises ValidationError with code="unsupported_region".

Full signal reference: zyfy.uk/docs/postcode.html

Returns: PostcodeResult


client.postcode.nearest(lat, lon, *, radius=None, max_enrichment_retries=None)

Find the nearest postcode to a set of WGS84 coordinates. Coordinates must be within the UK bounding box. The response includes a query_point_distance_metres field.

Parameter Type Default Description
lat float WGS84 latitude.
lon float WGS84 longitude.
radius int | None 1000 Search radius in metres. Raises NotFoundError if no postcode centroid is found within the radius.
max_enrichment_retries int | None Per-call override of max_enrichment_retries.

Returns: PostcodeResult


client.postcode.bulk_lookup(postcodes, *, max_enrichment_retries=None)

Synchronous bulk postcode lookup.

Returns: BulkPostcodeResult

Use is_postcode_bulk_error(item) to distinguish errors:

from zyfy import Zyfy, is_postcode_bulk_error

result = client.postcode.bulk_lookup(["SW1A 2AA", "BT1 1AA"])
for item in result.results:
    if is_postcode_bulk_error(item):
        print(item.postcode, item.error)  # "not_found" | "unsupported_region"
    else:
        print(item.postcode, item.summary.liveability_level if item.summary else None)

client.postcode.submit_bulk(postcodes) / get_job(job_id) / delete_job(job_id)

Same async bulk pattern as vehicle. submit_bulk returns BulkJobSubmitted, get_job returns BulkJobStatus[PostcodeResult | PostcodeBulkItemError], delete_job returns DeletedJob.


Error handling

All errors extend ZyfyError. Import the specific classes for isinstance checks.

import time
from zyfy import (
    Zyfy,
    QuotaExhaustedError,
    RateLimitError,
    ValidationError,
    NotFoundError,
    AuthenticationError,
    ApiError,
    NetworkError,
)

try:
    result = client.vehicle.lookup("AB12CDE")
except QuotaExhaustedError as exc:
    # Monthly quota exhausted. exc.resets is an ISO 8601 UTC string indicating
    # when the quota rolls over — None if no monthly reset applies.
    if exc.resets:
        hours_until_reset = round(exc.retry_after / 3600)
        print(f"Quota exhausted. Resets at {exc.resets} (~{hours_until_reset}h)")
    else:
        print("Quota exhausted. Contact support to increase your limit.")
except RateLimitError as exc:
    # Per-minute rate limit exceeded. Back off by exc.retry_after seconds and retry.
    print(f"Rate limited. Retrying in {exc.retry_after}s")
    time.sleep(exc.retry_after)
    # retry the call...
except ValidationError as exc:
    # Input rejected by the server. exc.code identifies the specific problem.
    # Common codes: "unsupported_region" (BT postcodes), "invalid_format"
    print(f"Validation error: {exc.code}")
except NotFoundError:
    print("Vehicle or postcode not found")
except AuthenticationError:
    print("Invalid or missing API key")
except ApiError as exc:
    print(f"Server error {exc.status_code}")
except NetworkError as exc:
    print(f"Connection failed: {exc.cause}")
Error class HTTP status When
AuthenticationError 401 Invalid or missing API key
NotFoundError 404 Vehicle or postcode not found
ValidationError 422 Invalid input; check exc.code (e.g. "unsupported_region", "invalid_format")
RateLimitError 429 Per-minute rate limit exceeded; exc.retry_after seconds until safe to retry
QuotaExhaustedError 429 Monthly quota exhausted; exc.retry_after seconds until reset, exc.resets ISO 8601 datetime (None if not applicable)
ApiError 5xx Server error; exc.status_code
NetworkError Connection failure or request timeout

All error classes expose raw_body: str with the full response body for debugging.

Quota

Every successful response includes a quota object populated from response headers:

result = client.vehicle.lookup("AB12CDE")
quota = result.quota

print(quota.limit)        # monthly cap — int, or "unlimited"
print(quota.used)         # requests consumed this month
print(quota.remaining)    # requests left — int, or "unlimited"
print(quota.grace_limit)  # buffer above limit before hard block (~10%); None if not applicable
print(quota.resets)       # ISO 8601 UTC string of next reset; None for unlimited plans

remaining reaches zero at the quota limit and further requests are blocked. A small grace buffer (grace_limit) allows a few extra requests above the cap before the hard block kicks in.

resets is None when no monthly cap is in effect. Always check for None before formatting or displaying it.

To avoid hitting the limit unexpectedly in high-volume applications, read quota.remaining after each response and slow down before it reaches zero.

Debug mode

Pass debug=True to log every request and response via the zyfy logger at DEBUG level:

import logging
logging.basicConfig(level=logging.DEBUG)

client = Zyfy(api_key="...", debug=True)

Output looks like:

[zyfy] → GET https://zyfy.uk/v1/vehicle/AB12CDE (X-Api-Key: ea_live_***, attempt 1)
[zyfy] ← 200 (quota-remaining: 9958, retry-after: none)

The API key is always redacted — it will never appear in logs regardless of debug mode. Useful for diagnosing unexpected responses, quota consumption, or enrichment retry behaviour in development.

Enrichment retries

Vehicle lookups may return enrichment_pending=True when background enrichment is still running — typically the first time a registration is seen, or when the vehicle's data is being refreshed. When pending, signals, summary, and scores may be None or incomplete. Postcode lookups are always served from a pre-loaded dataset and never set enrichment_pending.

Automatic retries (default)

The client retries automatically up to max_enrichment_retries times (default: 10), waiting the number of seconds in the Retry-After response header (typically 5 seconds) between each attempt. The final result is returned once enrichment completes or retries are exhausted — no exception is raised either way.

Override the retry limit per call:

result = client.vehicle.lookup("AB12CDE", max_enrichment_retries=3)

Manual retry pattern

If you need control over retry timing — for example, in a task queue where you want to reenqueue rather than block the worker — disable auto-retries and handle enrichment_pending yourself:

import time

client = Zyfy(api_key="...", max_enrichment_retries=0)

result = client.vehicle.lookup("AB12CDE")

if result.enrichment_pending:
    # Partial data returned — signals/summary/scores may be None.
    # The API will typically have the enriched result ready within 5 seconds.

    # Option A: wait and re-query inline
    time.sleep(5)
    result = client.vehicle.lookup("AB12CDE")

    # Option B: in a task queue, store result.registration and resubmit
    # the task after a delay rather than blocking here.

# Use whatever is available — enrichment_pending may still be True
# if enrichment is unusually slow. The data returned is always valid.
if result.enrichment_pending:
    print(f"Partial data for {result.registration} — enrichment still in progress")

recommendation = result.summary.buy_recommendation if result.summary else "pending"
print(result.registration, recommendation)

If auto-retries exhaust while enrichment_pending is still True, the last partial response is returned without raising. All returned fields are valid — only fields that depend on enrichment may be None.

Python type reference

All response types are frozen dataclasses and exported from the package.

Quota

Returned on every successful response. Populated from X-Quota-* response headers.

@dataclass(frozen=True)
class Quota:
    limit: int | str                  # int, or "unlimited"
    used: int
    remaining: int | str              # int, or "unlimited"
    grace_limit: int | None
    resets: str | None                # ISO 8601; None for unlimited plans

VehicleResult

@dataclass(frozen=True)
class VehicleResult:
    registration: str                 # uppercase, no spaces
    make: str | None
    model: str | None
    vehicle_type: str | None          # "car" | "van" | "motorcycle" | "bus" | "hgv" | "motorhome" | "trailer" | "tractor" | "other"
    colour: str | None
    fuel_type: str | None
    engine_capacity_cc: int | None
    year_of_manufacture: int | None
    month_of_first_registration: str | None   # YYYY-MM
    vehicle_age_years: float | None
    summary: VehicleSummary | None
    signals: VehicleSignals | None
    scores: VehicleScores | None
    fleet_failure_profile: FleetFailureProfile | None
    fleet_advisory_profile: FleetAdvisoryProfile | None
    sources: VehicleSources
    schema_version: str
    enrichment_pending: bool
    data_as_of: str                   # ISO 8601
    checked_at: str                   # ISO 8601
    quota: Quota | None               # None when returned as a bulk item

@dataclass(frozen=True)
class VehicleSummary:
    buy_recommendation: str | None    # "good" | "consider" | "caution" | "avoid"
    vehicle_risk_level: str | None    # "low" | "medium" | "high"
    mot_risk_level: str | None        # "low" | "medium" | "high"
    condition_band: str | None        # "good" | "fair" | "poor" | "bad"
    maintenance_band: str | None      # "good" | "fair" | "poor" | "bad"
    mileage_anomaly_risk: str | None  # "none" | "low" | "high"
    colour_change_indicated: bool | None
    above_average_advisories: bool | None
    mot_failure_detail_available: bool

@dataclass(frozen=True)
class VehicleSignals:
    co2_emissions_g_per_km: float | None
    euro_emission_standard: str | None
    ulez_compliant: bool | None
    marked_for_export: bool
    has_outstanding_recall: bool | None
    v5c_last_issued: str | None            # YYYY-MM-DD
    tax_status: str | None
    tax_due_date: str | None               # YYYY-MM-DD
    tax_days_remaining: int | None
    ved_band: str | None
    ved_annual_cost_gbp: float | None
    mot_status: str | None
    mot_expiry_date: str | None            # YYYY-MM-DD
    mot_days_remaining: int | None
    imminent_mot: bool
    odometer_trend: str | None             # "consistent" | "increasing" | "decreasing" | "erratic"
    latest_odometer_miles: int | None
    typical_annual_mileage_miles: int | None
    odometer_vs_fleet_average: str | None  # "below_average" | "average" | "above_average"
    mot_pass_rate: float | None
    total_mot_tests: int
    total_mot_failures: int
    total_advisory_count: int
    total_failure_item_count: int
    latest_advisory_count: int
    latest_failure_item_count: int
    dangerous_defect_ever: bool
    high_failure_history: bool
    advisory_trend: str | None             # "increasing" | "stable" | "decreasing"
    advisory_momentum: str | None          # "worsening" | "stable" | "improving"
    days_since_last_failure: int | None
    failures_last_24_months: int | None
    advisories_last_3_tests: int | None
    trend_window_tests: int
    first_mot_date: str | None             # YYYY-MM-DD
    last_mot_date: str | None              # YYYY-MM-DD
    last_mot_result: str | None
    first_mot_due: str | None              # YYYY-MM-DD
    failure_clusters: list[str] | None
    repeat_failure_count: int | None
    advisory_clusters: list[str] | None
    ncap_safety_rating: NcapSafetyRating | None
    drivetrain_stress_profile: DrivetrainStressProfile | None

@dataclass(frozen=True)
class NcapSafetyRating:
    overall_stars: int | None            # 0–5
    adult_occupant: float | None         # 0–100
    child_occupant: float | None         # 0–100
    vulnerable_road_users: float | None  # 0–100
    safety_assist: float | None          # 0–100
    tested_year: int | None

@dataclass(frozen=True)
class DrivetrainStressProfile:
    likely_driving_pattern: str | None   # "short_urban" | "mixed" | "long_distance"
    dpf_risk: str | None                 # "low" | "elevated" | "high" (diesel only)

@dataclass(frozen=True)
class VehicleScores:
    mot_risk_score: float | None         # 0–1, lower is better
    condition_score: float | None        # 0–1, higher is better
    condition_percentile: float | None
    maintenance_score: float | None      # 0–1, higher is better
    maintenance_percentile: float | None
    failure_rate_ratio: float | None     # 1.0 = fleet average
    advisory_rate_ratio: float | None    # 1.0 = fleet average
    benchmark_sample_size: int | None
    avg_advisories_per_test_for_mmy: float | None
    avg_failures_per_test_for_mmy: float | None
    off_road_likelihood_score: float | None  # 0–1, higher = more likely off-road
    score_convention: str

@dataclass(frozen=True)
class FleetFailureProfile:
    mileage_band: str
    sample_size: int
    top_failures: list[FleetItem]

@dataclass(frozen=True)
class FleetAdvisoryProfile:
    mileage_band: str
    sample_size: int
    top_advisories: list[FleetItem]

@dataclass(frozen=True)
class FleetItem:
    category: str
    rate: float

@dataclass(frozen=True)
class VehicleSources:
    mot_history: str
    mutable_data: str
    safety_rating: str | None

PostcodeResult

@dataclass(frozen=True)
class PostcodeResult:
    postcode: str                     # formatted with space: "SW1A 2AA"
    outward_code: str
    inward_code: str | None           # None for outward-code-only queries
    latitude: float | None
    longitude: float | None
    eastings: int | None
    northings: int | None
    country: str | None
    region: str | None
    admin_district: str | None
    admin_county: str | None
    admin_ward: str | None
    parish: str | None
    parliamentary_constituency: str | None
    nhs_trust: str | None
    lsoa: str | None
    msoa: str | None
    rural_urban_classification: str | None
    summary: PostcodeSummary | None
    signals: PostcodeSignals | None
    scores: PostcodeScores | None
    percentiles: PostcodePercentiles | None   # Starter+ tiers only
    geography_codes: GeographyCodes
    query_point_distance_metres: float | None # nearest endpoint only
    sources: PostcodeSources
    schema_version: str
    data_as_of: str                   # ISO 8601
    checked_at: str                   # ISO 8601
    quota: Quota | None               # None when returned as a bulk item

@dataclass(frozen=True)
class PostcodeSummary:
    property_risk_level: str | None   # "low" | "medium" | "high"
    liveability_level: str | None     # "low" | "medium" | "high"
    insurance_risk_level: str | None  # "low" | "medium" | "high"
    investment_outlook: str | None    # "weak" | "fair" | "good" | "strong"
    growth_signal: str | None         # "strong_positive" | "positive" | "neutral" | "weak_negative" | "strong_negative"
    data_confidence: str              # "low" | "medium" | "high"
    area_trajectory: str | None       # "improving" | "stable" | "declining" | "mixed"
    family_suitability: str | None    # "poor" | "fair" | "good" | "excellent"
    retirement_suitability: str | None

@dataclass(frozen=True)
class PostcodeSignals:
    broadband: PostcodeBroadbandSignals | None
    flood: PostcodeFloodSignals | None
    property: PostcodePropertySignals | None
    crime: PostcodeCrimeSignals | None
    environment: PostcodeEnvironmentSignals | None
    housing: PostcodeHousingSignals | None
    political: PostcodePoliticalSignals | None
    deprivation: PostcodeDeprivationSignals | None
    demographics: PostcodeDemographicsSignals | None

@dataclass(frozen=True)
class PostcodeBroadbandSignals:
    superfast: bool | None
    ultrafast: bool | None
    gigabit: bool | None

@dataclass(frozen=True)
class PostcodeFloodSignals:
    rivers_sea: str | None            # "high" | "medium" | "low" | "very_low"
    groundwater: str | None           # "high" | "medium" | "low" | "very_low"
    rivers_sea_trend: str | None      # "worsening" | "stable" | "improving" | "insufficient_data"
    groundwater_trend: str | None

@dataclass(frozen=True)
class PostcodePropertySignals:
    average_price: float | None       # median price, last 12 months
    price_low: float | None           # 25th percentile
    price_high: float | None          # 75th percentile
    price_trend: float | None         # YoY % change, e.g. 5.2 = +5.2%
    price_trend_period_months: int
    transaction_volume: int | None    # residential transactions, last 12 months
    granularity: str | None           # "postcode" | "sector" | "district"
    trend_confidence: str | None      # "high" | "medium" | "low"

@dataclass(frozen=True)
class PostcodeCrimeSignals:
    rate_band: str | None             # "very_low" | "low" | "medium" | "high" | "very_high"
    data_granularity: PostcodeCrimeGranularity | None
    categories: PostcodeCrimeCategories | None

@dataclass(frozen=True)
class PostcodeCrimeGranularity:
    band: str | None                  # "lsoa" | "datazone"
    categories: str | None            # "lsoa" | "local_authority"

@dataclass(frozen=True)
class PostcodeCrimeCategories:
    violence: PostcodeCrimeCategory | None
    property: PostcodeCrimeCategory | None
    vehicle: PostcodeCrimeCategory | None
    antisocial: PostcodeCrimeCategory | None
    drugs: PostcodeCrimeCategory | None
    damage: PostcodeCrimeCategory | None

@dataclass(frozen=True)
class PostcodeCrimeCategory:
    band: str | None                  # "very_low" | "low" | "medium" | "high" | "very_high"
    trend: str | None                 # "increasing" | "stable" | "decreasing" | "insufficient_data"

@dataclass(frozen=True)
class PostcodeEnvironmentSignals:
    air_quality_band: str | None      # "very_low" | "low" | "moderate" | "high"
    air_quality_trend: str | None
    no2_ug_m3: float | None           # annual mean NO2 µg/m³
    pm25_ug_m3: float | None          # annual mean PM2.5 µg/m³
    radon_potential: str | None       # "very_low" | "low" | "medium" | "high" | "very_high"
    green_space_proximity_metres: float | None
    is_national_park: bool | None
    is_aonb: bool | None
    is_green_belt: bool | None

@dataclass(frozen=True)
class PostcodeHousingSignals:
    epc_average_rating: str | None         # A–G
    council_tax_band: CouncilTaxBandEstimate | None
    dominant_property_type: str | None     # "detached" | "semi_detached" | "terraced" | "flat" | "other"

@dataclass(frozen=True)
class CouncilTaxBandEstimate:
    lower: str                        # A–I
    upper: str                        # A–I; equals lower when a single band is determined
    source: str                       # "exact_nrs" | "derived_hmlr" | "derived_lsoa"

@dataclass(frozen=True)
class PostcodePoliticalSignals:
    mp_name: str | None
    mp_party: str | None
    mp_party_colour: str | None

@dataclass(frozen=True)
class PostcodeDeprivationSignals:
    imd_decile: int | None            # 1 = most deprived, 10 = least deprived; England only
    imd_trend: str | None             # "improving" | "stable" | "declining" | "insufficient_data"

@dataclass(frozen=True)
class PostcodeDemographicsSignals:
    perc_owner_occupied: float | None
    perc_private_rented: float | None
    perc_no_car_van: float | None
    median_age: float | None
    perc_economically_active: float | None

@dataclass(frozen=True)
class PostcodeScores:
    property_risk_score: float | None  # 0–1, lower is better
    liveability_score: float | None    # 0–1, higher is better
    investment_score: float | None     # 0–1, higher is better
    affordability_index: float | None  # 0–1, higher is better
    score_convention: str

@dataclass(frozen=True)
class PostcodePercentiles:
    flood_rivers: float | None
    flood_groundwater: float | None
    crime_rate: float | None
    imd: float | None
    property_price: float | None
    property_price_regional: float | None
    radon: float | None
    air_quality: float | None
    green_space_proximity: float | None
    epc: float | None

@dataclass(frozen=True)
class GeographyCodes:
    admin_district: str | None
    admin_county: str | None
    admin_ward: str | None
    parliamentary_constituency: str | None
    lsoa: str | None
    msoa: str | None

@dataclass(frozen=True)
class PostcodeSources:
    geography: str
    flood: str
    crime: str
    property: str
    deprivation: str
    broadband: str
    environment: str
    epc: str
    green_space: str
    demographics: str | None

Bulk types

@dataclass(frozen=True)
class BulkVehicleResult:
    total: int
    results: list[VehicleResult | VehicleBulkItemError]
    quota: Quota

@dataclass(frozen=True)
class BulkPostcodeResult:
    total: int
    results: list[PostcodeResult | PostcodeBulkItemError]
    quota: Quota

@dataclass(frozen=True)
class VehicleBulkItemError:
    registration: str
    error: str                        # "not_found" | "invalid_format"

@dataclass(frozen=True)
class PostcodeBulkItemError:
    postcode: str
    error: str                        # "not_found" | "unsupported_region" | "invalid_format"

@dataclass(frozen=True)
class BulkJobSubmitted:
    job_id: str
    status: str                       # "queued"
    total: int
    poll_url: str
    quota: Quota

@dataclass(frozen=True)
class BulkJobStatus(Generic[T]):
    job_id: str
    status: str                       # "queued" | "processing" | "complete" | "expired"
    total: int
    done: int
    created_at: str                   # ISO 8601
    completed_at: str | None          # ISO 8601; None until complete
    expires_at: str                   # ISO 8601
    results: list[T] | None           # None until status == "complete"
    quota: Quota

@dataclass(frozen=True)
class DeletedJob:
    deleted: str                      # job_id that was deleted
    quota: Quota

Type guards

def is_vehicle_bulk_error(
    item: VehicleResult | VehicleBulkItemError,
) -> TypeGuard[VehicleBulkItemError]: ...

def is_postcode_bulk_error(
    item: PostcodeResult | PostcodeBulkItemError,
) -> TypeGuard[PostcodeBulkItemError]: ...

Examples

Runnable examples are in the examples/ directory.

# Set your key first
export ZYFY_API_KEY=ea_live_...

# Single lookups
VEHICLE_REG=AB12CDE python3 examples/vehicle.py
POSTCODE="SW1A 2AA" python3 examples/postcode.py

# Geographic queries
LAT=51.508 LON=-0.1281 python3 examples/nearest.py
LAT=51.508 LON=-0.1281 RADIUS=500 python3 examples/within.py

# Bulk lookups
VEHICLE_REGS=AB12CDE,XY34FGH python3 examples/bulk_vehicle.py
POSTCODES="SW1A 2AA,M1 1AE" python3 examples/bulk_postcode.py

# Error handling scenarios
python3 examples/errors.py vehicle-invalid
python3 examples/errors.py postcode-not-found
python3 examples/errors.py postcode-ni
python3 examples/errors.py bad-auth

Versioning

This library follows SemVer. See CHANGELOG.md for version history.

0.x.x releases may include breaking changes between minor versions. Stability guaranteed from 1.0.0.


zyfy.uk · Docs · Sign up · GitHub

About

Official Python client library for the Zyfy UK data enrichment API

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages