Skip to main content

Async Python SDK for AdventureLog — the self-hosted travel tracker

Project description

py-adventurelog

PyPI version Python versions License

An async-first Python SDK for AdventureLog — the self-hosted travel tracker. Built on httpx and Pydantic v2, fully typed, and designed as the foundation layer for Telegram/WhatsApp bots and automation scripts.

Features

  • Async-first — every resource method is a native coroutine or async generator; the event loop is never blocked
  • Sync wrapper includedAdventureLog runs the event loop internally for scripts that don't need asyncio
  • Fully typed — Pydantic v2 models for every response; full mypy --strict compatibility
  • Auto-paginationlist() methods follow DRF next links transparently; you consume a single async generator
  • Stateless-friendly — pass a pre-obtained token to skip the login round-trip (ideal for multi-user bots)
  • Resilient — configurable per-request timeout and automatic retry with exponential back-off on 5xx / transport errors

Installation

pip install py-adventurelog

Requirements: Python 3.10+ · httpx ≥ 0.27 · pydantic ≥ 2

From source

git clone https://github.com/t0mer/py-adventurelog.git
cd py-adventurelog
pip install -e ".[dev]"

Quick Start

import asyncio
from adventurelog import AsyncAdventureLog

async def main():
    async with AsyncAdventureLog(
        base_url="https://your-adventurelog-server.com",
        username="you@example.com",
        password="s3cr3t",
    ) as al:
        me = await al.user.me()
        print(f"Logged in as {me.username}")

        async for location in al.locations.list(is_visited=True):
            print(location.name, location.country)

asyncio.run(main())

Synchronous usage

from adventurelog import AdventureLog

with AdventureLog(
    base_url="https://your-adventurelog-server.com",
    username="you@example.com",
    password="s3cr3t",
) as al:
    locations = al.locations.list(is_visited=True)   # returns list[Location]
    print(f"You have visited {len(locations)} places")

Note: Async generators (e.g. locations.list()) are fully materialised into a list when used through the sync wrapper.

Configuration

Constructor arguments

Argument Type Default Description
base_url str required AdventureLog server root URL (no trailing slash)
username str None Login username — required when token is not provided
password str None Login password — required when token is not provided
token str None Pre-obtained sessionid cookie — skips login round-trip
timeout float 30.0 Per-request timeout in seconds
max_retries int 3 Retry attempts on 5xx / transport errors

Environment variables

The helper ClientConfig.from_env() reads these variables so you don't have to pass credentials in code:

Variable Required Description
ADVENTURELOG_BASE_URL yes Server root URL
ADVENTURELOG_USERNAME one of these Login username
ADVENTURELOG_PASSWORD one of these Login password
ADVENTURELOG_SESSION_TOKEN or this Pre-obtained session token
from adventurelog.config import ClientConfig
from adventurelog import AsyncAdventureLog

config = ClientConfig.from_env()
async with AsyncAdventureLog(
    base_url=config.base_url,
    username=config.username,
    password=config.password,
) as al:
    ...

Usage Examples

Listing visited locations with pagination

async with AsyncAdventureLog(...) as al:
    async for loc in al.locations.list(is_visited=True, page_size=50):
        print(f"{loc.name}{loc.country}")

Fetching a single page

async with AsyncAdventureLog(...) as al:
    page = await al.locations.page(page=2, page_size=20)
    print(f"Page 2 of {page.count} locations")
    for loc in page.results:
        print(loc.name)

Creating a location

async with AsyncAdventureLog(...) as al:
    loc = await al.locations.create({
        "name": "Colosseum",
        "description": "Ancient amphitheatre in Rome",
        "is_visited": True,
        "latitude": 41.8902,
        "longitude": 12.4922,
    })
    print(f"Created: {loc.id}")

Organising a trip with collections

async with AsyncAdventureLog(...) as al:
    trip = await al.collections.create({"name": "Italy 2025", "is_archived": False})

    async for col in al.collections.list():
        print(col.name)

    shared = [c async for c in al.collections.shared()]

Working with a pre-obtained token (bot use case)

# Obtained and cached from a previous session:
TOKEN = "abc123sessionid"

async with AsyncAdventureLog(
    base_url="https://your-server.com",
    token=TOKEN,
) as al:
    me = await al.user.me()

Recording a visit

async with AsyncAdventureLog(...) as al:
    visit = await al.visits.create({
        "location": "uuid-of-location",
        "start_date": "2025-06-01",
        "end_date": "2025-06-03",
    })

Geo lookups

async with AsyncAdventureLog(...) as al:
    countries = await al.geo.countries()
    regions = await al.geo.regions()
    visited = await al.geo.visited_cities()
    await al.geo.create_visited_city({"city": 42})

Managing API keys

async with AsyncAdventureLog(...) as al:
    new_key = await al.user.create_api_key("my-bot")
    print(new_key["key"])   # shown once only — store it now

    keys = await al.user.api_keys()
    await al.user.delete_api_key(keys[0].id)

API Reference

All resource namespaces are available as attributes of the client after entering the context manager.


al.locationsLocationsResource

Method Returns Description
list(*, page_size=20, **params) AsyncIterator[Location] Stream all locations, follows pagination
page(*, page=1, page_size=20, **params) PaginatedResponse[Location] Fetch a single page
get(id) Location Retrieve by UUID
create(data) Location Create a new location
update(id, data) Location Full replace
partial_update(id, data) Location Partial update
delete(id) None Delete
all_locations(*, page_size=100, **params) AsyncIterator[Location] Stream via /all/ endpoint (no visibility filters)
quick_add(data) Location Create via quick-add shortcut
duplicate(id) Location Duplicate an existing location

al.collectionsCollectionsResource

Method Returns Description
list(*, page_size=20, **params) AsyncIterator[Collection] Stream all collections
page(*, page=1, page_size=20, **params) PaginatedResponse[Collection] Fetch a single page
get(id) Collection Retrieve by UUID
create(data) Collection Create a new collection
update(id, data) Collection Full replace
partial_update(id, data) Collection Partial update
delete(id) None Delete
archived(*, page_size=20, **params) AsyncIterator[Collection] Stream archived collections
shared(*, page_size=20, **params) AsyncIterator[Collection] Stream collections shared with current user
duplicate(id) Collection Duplicate a collection

al.activitiesActivitiesResource

Method Returns Description
list() list[Activity] All activities for current user
get(id) Activity Retrieve by UUID
create(data) Activity Create a new activity
update(id, data) Activity Full replace
partial_update(id, data) Activity Partial update
delete(id) None Delete

al.visitsVisitsResource

Method Returns Description
list() list[Visit] All visits for current user
get(id) Visit Retrieve by UUID
create(data) Visit Record a new visit
update(id, data) Visit Full replace
partial_update(id, data) Visit Partial update
delete(id) None Delete

al.notesNotesResource

Method Returns Description
list() list[Note] Notes for current user's collections
all_notes() list[Note] All notes including shared collections
get(id) Note Retrieve by UUID
create(data) Note Create a new note
update(id, data) Note Full replace
partial_update(id, data) Note Partial update
delete(id) None Delete

al.checklistsChecklistsResource

Method Returns Description
list() list[Checklist] All checklists for current user
get(id) Checklist Retrieve by UUID
create(data) Checklist Create a new checklist
update(id, data) Checklist Full replace
partial_update(id, data) Checklist Partial update
delete(id) None Delete

al.lodgingLodgingResource

Method Returns Description
list() list[Lodging] All lodging records for current user
get(id) Lodging Retrieve by UUID
create(data) Lodging Create a lodging record
update(id, data) Lodging Full replace
partial_update(id, data) Lodging Partial update
delete(id) None Delete

al.transportationsTransportationsResource

Method Returns Description
list() list[Transportation] All transportation records
get(id) Transportation Retrieve by UUID
create(data) Transportation Create a transportation record
update(id, data) Transportation Full replace
partial_update(id, data) Transportation Partial update
delete(id) None Delete

al.trailsTrailsResource

Method Returns Description
list() list[Trail] All trails for current user
get(id) Trail Retrieve by UUID
create(data) Trail Create a trail
update(id, data) Trail Full replace
partial_update(id, data) Trail Partial update
delete(id) None Delete

al.imagesImagesResource

Method Returns Description
list() list[ContentImage] All images for current user
get(id) ContentImage Retrieve by UUID
create(data) ContentImage Create an image record
update(id, data) ContentImage Full replace
partial_update(id, data) ContentImage Partial update
delete(id) None Delete

al.categoriesCategoriesResource

Method Returns Description
list() list[Category] All location categories
get(id) Category Retrieve by UUID
create(data) Category Create a category
update(id, data) Category Full replace
partial_update(id, data) Category Partial update
delete(id) None Delete

al.geoGeoResource

Method Returns Description
countries() list[Country] All countries
country(id) Country Retrieve country by integer ID
regions() list[Region] All regions (states/provinces)
region(id) Region Retrieve region by integer ID
visited_cities() list[VisitedCity] Cities marked as visited
create_visited_city(data) VisitedCity Mark a city as visited
delete_visited_city(id) None Remove a visited-city record
visited_regions() list[VisitedRegion] Regions marked as visited
create_visited_region(data) VisitedRegion Mark a region as visited
delete_visited_region(id) None Remove a visited-region record

al.itinerariesItinerariesResource

Itinerary items (/api/itineraries/)

Method Returns Description
list_items() list[CollectionItineraryItem] All itinerary items
get_item(id) CollectionItineraryItem Retrieve by UUID
create_item(data) CollectionItineraryItem Create an itinerary item
update_item(id, data) CollectionItineraryItem Full replace
partial_update_item(id, data) CollectionItineraryItem Partial update
delete_item(id) None Delete

Itinerary days (/api/itinerary-days/)

Method Returns Description
list_days() list[CollectionItineraryDay] All itinerary days
get_day(id) CollectionItineraryDay Retrieve by UUID
create_day(data) CollectionItineraryDay Create an itinerary day
update_day(id, data) CollectionItineraryDay Full replace
partial_update_day(id, data) CollectionItineraryDay Partial update
delete_day(id) None Delete

al.userUserResource

Method Returns Description
me() CustomUserDetails Current authenticated user profile
update_profile(data) CustomUserDetails Partially update user profile
api_keys() list[APIKey] All API keys (prefix/metadata only)
create_api_key(name) dict Create API key — full key returned once
delete_api_key(id) None Revoke and delete an API key

Error Handling

All SDK errors inherit from AdventureLogError. Raw httpx exceptions never escape to callers.

from adventurelog import (
    AsyncAdventureLog,
    AdventureLogError,
    AuthenticationError,
    NotFoundError,
    ValidationError,
    PermissionDenied,
    RateLimitError,
    ServerError,
    APIConnectionError,
)

async with AsyncAdventureLog(...) as al:
    try:
        loc = await al.locations.get("non-existent-id")
    except NotFoundError:
        print("Location not found")
    except ValidationError as e:
        print("Validation failed:", e.field_errors)
    except AuthenticationError:
        print("Login failed or session expired")
    except RateLimitError:
        print("Too many requests — back off and retry")
    except ServerError:
        print("Server returned 5xx")
    except APIConnectionError:
        print("Network or timeout error")
    except AdventureLogError as e:
        print("Unexpected SDK error:", e)

Exception hierarchy

AdventureLogError
├── AuthenticationError     # HTTP 401 — login failed or session expired
├── PermissionDenied        # HTTP 403 — insufficient permissions
├── NotFoundError           # HTTP 404 — resource does not exist
├── ValidationError         # HTTP 400/422 — carries .field_errors dict
├── RateLimitError          # HTTP 429 — server rate limit hit
├── ServerError             # HTTP 5xx — server-side failure
└── APIConnectionError      # Network / timeout error (wraps httpx)

Contributing

  1. Fork the repository and create a branch from main
  2. Install dev dependencies: pip install -e ".[dev]"
  3. Make your changes; add or update tests in tests/unit/
  4. Ensure all checks pass:
ruff check src/ tests/
ruff format src/ tests/
mypy --strict src/
pytest tests/unit/
  1. Open a pull request against main

Running the tests

# Unit tests (offline, no credentials needed)
pytest tests/unit/

# Integration tests (require a live server)
# Copy env.example to .env and fill in your credentials
pytest tests/integration/ -m integration

License

Distributed under the Apache License 2.0.

Author

Tomer Klein


py-adventurelog is an independent open-source project and is not affiliated with or endorsed by the AdventureLog project.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py_adventurelog-0.1.1.tar.gz (45.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

py_adventurelog-0.1.1-py3-none-any.whl (49.8 kB view details)

Uploaded Python 3

File details

Details for the file py_adventurelog-0.1.1.tar.gz.

File metadata

  • Download URL: py_adventurelog-0.1.1.tar.gz
  • Upload date:
  • Size: 45.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.25

File hashes

Hashes for py_adventurelog-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e0c7c8c96a3d017dd3c2fa1c12c701402b48b31c77defa8badabf3c8d0909258
MD5 63e514ee2b475ada6c3f89a73d699586
BLAKE2b-256 e7d82c1c1ec36b8aa4c3814ddd55aa4def1c5fabd95bf2fe763e971406c4770f

See more details on using hashes here.

File details

Details for the file py_adventurelog-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for py_adventurelog-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ec9ad96f8c0592c9e633902f4e2668ea480bb717aeeadcd6fbc9fb6d4a582641
MD5 3ef854ed60070799649dc38c7b2f3ed5
BLAKE2b-256 17d539befe3057802dcd235d8aa5f1a214b99b966f757129874dd8272c68863e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page