Skip to main content

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 included — AdventureLog 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-pagination — list() 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
  • Full API coverage — all 112 endpoints across 21 resource namespaces

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 and sharing a collection

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

    # Check sharing eligibility, then share with a user
    info = await al.collections.can_share(trip.id)
    if info.get("can_share"):
        await al.collections.share(trip.id, "user-uuid-here")

    # Accept a pending invite
    await al.collections.accept_invite("collection-uuid")
async with AsyncAdventureLog(...) as al:
    place = await al.reverse_geocode.reverse_geocode(lat=41.89, lng=12.49)
    results = await al.reverse_geocode.search(query="Colosseum Rome")
    await al.reverse_geocode.mark_visited_region(lat=41.89, lng=12.49)

AI generation

async with AsyncAdventureLog(...) as al:
    desc = await al.generate.description(location_id="uuid-here")
    img  = await al.generate.image(location_id="uuid-here")
    recs = await al.generate.recommendations(query="beaches in Italy")

    # Download ICS calendar
    ics_bytes = await al.generate.ics_calendar()
    with open("adventures.ics", "wb") as f:
        f.write(ics_bytes)

Integrations (Immich, Strava, Wanderer)

async with AsyncAdventureLog(...) as al:
    # Immich — browse albums and import photos
    albums = await al.integrations.immich_albums()
    results = await al.integrations.immich_search(query="Paris")

    # Strava — import fitness activities
    auth_url = await al.integrations.strava_authorize()
    activities = await al.integrations.strava_activities()

    # Wanderer — sync trails
    await al.integrations.wanderer_refresh()
    trails = await al.integrations.wanderer_trails()

Backup export and import

async with AsyncAdventureLog(...) as al:
    data = await al.backup.export()
    with open("backup.zip", "wb") as f:
        f.write(data)

Global search and stats

async with AsyncAdventureLog(...) as al:
    results = await al.search.search(query="Tokyo")
    tag_types = await al.search.tag_types()
    counts = await al.stats.counts("myusername")
    print(f"Visited {counts['visited_location_count']} locations")

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 21 resource namespaces are available as attributes on the client after entering the context manager.


al.locations — LocationsResource

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/ (no visibility filter)
quick_add(data) Location Create via quick-add shortcut
duplicate(id) Location Duplicate an existing location
calendar(*, page_size=20, **params) AsyncIterator[Location] Stream calendar view
filtered(*, page_size=20, **params) AsyncIterator[Location] Stream filtered locations
pins(*, page_size=20, **params) AsyncIterator[Location] Stream map-pin locations
additional_info(id) dict Retrieve extra metadata for a location

al.collections — CollectionsResource

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
all_collections(*, page_size=20, **params) AsyncIterator[Collection] Stream via /all/
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
import_collection(data) Collection Import a collection from form data
invites(*, page_size=20, **params) AsyncIterator[Collection] Stream pending invites
can_share(id) dict Check whether the collection can be shared
export(id) bytes Download export file
share(id, user_uuid) dict Share with a user
unshare(id, user_uuid) dict Remove sharing with a user
revoke_invite(id, invite_uuid) dict Revoke a pending invite
accept_invite(id) dict Accept an invite
decline_invite(id) dict Decline an invite
leave(id) dict Leave a shared collection

al.activities — ActivitiesResource

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.visits — VisitsResource

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.notes — NotesResource

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.checklists — ChecklistsResource

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.lodging — LodgingResource

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
quick_add(data) Lodging Create via quick-add shortcut

al.transportations — TransportationsResource

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.trails — TrailsResource

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.images — ImagesResource

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
fetch_from_url(data) ContentImage Fetch and create an image from a remote URL
import_from_urls(data) list[ContentImage] Bulk-import images from a list of URLs
image_delete(id) dict Delete the file associated with an image record
toggle_primary(id) ContentImage Toggle the primary flag on an image

al.attachments — AttachmentsResource

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

al.categories — CategoriesResource

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.geo — GeoResource

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
check_point_in_region(**params) dict Check whether a lat/lng falls in a region
region_check_all_adventures(data) dict Check all adventures against region boundaries
cities_in_region(region_id) list[dict] All cities within a region
city_visits_in_region(region_id) list[dict] Visited cities within a region
regions_by_country(country_code) list[dict] Regions for a country code
visits_by_country(country_code) list[dict] Visits for a country code

al.itineraries — ItinerariesResource

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
auto_generate(data) dict Auto-generate an itinerary for a collection
reorder(data) dict Reorder itinerary items

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.user — UserResource

Method Returns Description
me() CustomUserDetails Current authenticated user profile
update_profile(data) CustomUserDetails Partially update user profile
get_user(username) CustomUserDetails Retrieve a user's public profile by username
users() list[CustomUserDetails] All users (admin)
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
is_registration_disabled() dict Check whether public registration is disabled
social_providers() list[dict] Configured social auth providers
disable_password() dict Disable password login for current user
enable_password() None Re-enable password login
mobile_qr() dict Get the mobile QR login code
create_mobile_qr() dict Generate a new mobile QR login code
delete_mobile_qr() None Delete the mobile QR login code

al.reverse_geocode — ReverseGeocodeResource

Method Returns Description
reverse_geocode(**params) dict Reverse-geocode a lat/lng to a place name
place_details(**params) dict Retrieve detailed information about a place
search(**params) dict Search for places by name or query string
mark_visited_region(**params) dict Mark the region at a lat/lng as visited

al.integrations — IntegrationsResource

General

Method Returns Description
list() list[dict] All configured integrations

Immich (self-hosted photo management)

Method Returns Description
immich_list() list[dict] All Immich integration configs
immich_create(data) dict Create an Immich integration
immich_get(id) dict Retrieve by UUID
immich_update(id, data) dict Full replace
immich_partial_update(id, data) dict Partial update
immich_delete(id) None Delete
immich_albums() list[dict] All albums from Immich
immich_album(album_id) dict Retrieve a single album
immich_search(**params) dict Search Immich assets
immich_get_image(integration_id, image_id) dict Retrieve a specific Immich image

Strava (fitness activity tracking)

Method Returns Description
strava_activities() list[dict] Imported Strava activities
strava_activity(activity_id) dict Retrieve a single Strava activity
strava_authorize() dict Initiate OAuth authorization flow
strava_callback(**params) dict Handle OAuth callback
strava_disable() dict Disconnect Strava

Wanderer (trail/route tracking)

Method Returns Description
wanderer_create(data) dict Create / connect Wanderer integration
wanderer_update(id, data) dict Update Wanderer config
wanderer_trails() list[dict] Trails imported from Wanderer
wanderer_refresh() dict Re-sync trails from Wanderer
wanderer_disable() dict Disconnect Wanderer

al.generate — GenerateResource

Method Returns Description
description(**params) dict AI-generated description for a location
image(**params) dict AI-generated image for a location
ics_calendar(**params) bytes ICS calendar export
globespin(**params) dict Globe-spin data for 3-D visualisation
recommendations(**params) dict AI-powered location recommendations

al.backup — BackupResource

Method Returns Description
export() bytes Download full data backup
import_backup(data) dict Restore data from a backup

al.search — SearchResource

Method Returns Description
search(**params) dict Full-text search across all content
tag_types() list[dict] Available tag type values

al.stats — StatsResource

Method Returns Description
counts(username) dict Adventure count statistics for a user

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.

Metadata

Release files for py-adventurelog 0.1.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for py-adventurelog 0.1.3
File Size Uploaded
py_adventurelog-0.1.3.tar.gz 54.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for py-adventurelog 0.1.3
File Interpreter ABI Platform
py_adventurelog-0.1.3-py3-none-any.whl Python 3 none any Details

Total release size: 114.8 kB

Release files / py_adventurelog-0.1.3.tar.gz

Download URL py_adventurelog-0.1.3.tar.gz
Size 54.7 kB
Tags Source
SHA-256 checksum
How to use checksums
5bab44279dbd7cb8e1459ae0dfe139da50257b8d23b64da256251f5054be096d
BLAKE2b-256 checksum
How to use checksums
f4ee4c2867447b341dd2af254ac005eebbb8de30936619bde7e1861153e1ba57
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.25

Release files / py_adventurelog-0.1.3-py3-none-any.whl

Download URL py_adventurelog-0.1.3-py3-none-any.whl
Size 60.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
70ea0528398e0237eb981e7a910d616897550a178303cea43f44de3d57da33ae
BLAKE2b-256 checksum
How to use checksums
4a0379b5827072be58ec341f936df98f3d22875f37cfdfa075c26ba66fac1aa5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.25

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page