Skip to content

Client

The client classes provide full coverage of the Harvest Forecast API. The async client (AsyncForecastClient, exported as ForecastClient) is the canonical implementation; the sync client (SyncForecastClient) is generated from it via unasync.

Both clients share identical method signatures — the only difference is await for async methods.

Async client

AsyncForecastClient

AsyncForecastClient(
    access_token: str,
    account_id: str,
    user_agent: str,
    *,
    base_url: str = "https://api.forecastapp.com",
    timeout: float = 30.0,
    retry: RetryPolicy | None = None,
)

Async client for the Harvest Forecast API.

Example

async with AsyncForecastClient( access_token="token", account_id="123", user_agent="my-app (you@example.com)", ) as client: people = await client.list_people()

Initialize the async Forecast client.

Parameters:

Name Type Description Default
access_token str

Forecast personal access token.

required
account_id str

Forecast account ID.

required
user_agent str

User-Agent header value sent with every request.

required
base_url str

Forecast API base URL.

'https://api.forecastapp.com'
timeout float

Request timeout in seconds.

30.0
retry RetryPolicy | None

Retry policy for transient failures.

None
Source code in src/harvest_forecast/_async/client.py
def __init__(
    self,
    access_token: str,
    account_id: str,
    user_agent: str,
    *,
    base_url: str = "https://api.forecastapp.com",
    timeout: float = 30.0,
    retry: RetryPolicy | None = None,
) -> None:
    """Initialize the async Forecast client.

    Args:
        access_token: Forecast personal access token.
        account_id: Forecast account ID.
        user_agent: User-Agent header value sent with every request.
        base_url: Forecast API base URL.
        timeout: Request timeout in seconds.
        retry: Retry policy for transient failures.
    """
    self._retry = retry or RetryPolicy()
    self._account_id = account_id
    self._client = httpx.AsyncClient(
        base_url=base_url,
        timeout=httpx.Timeout(timeout, connect=10.0),
        headers={
            "Authorization": f"Bearer {access_token}",
            "Forecast-Account-ID": account_id,
            "User-Agent": user_agent,
            "Accept": "application/json",
        },
        limits=httpx.Limits(max_connections=8, max_keepalive_connections=4),
        follow_redirects=False,
    )

__aenter__ async

__aenter__() -> Self

Enter the async context manager.

Source code in src/harvest_forecast/_async/client.py
async def __aenter__(self) -> Self:
    """Enter the async context manager."""
    return self

__aexit__ async

__aexit__(*_: object) -> None

Exit the async context manager, closing the HTTP client.

Source code in src/harvest_forecast/_async/client.py
async def __aexit__(self, *_: object) -> None:
    """Exit the async context manager, closing the HTTP client."""
    await self.aclose()

aclose async

aclose() -> None

Close the underlying HTTP client.

Source code in src/harvest_forecast/_async/client.py
async def aclose(self) -> None:
    """Close the underlying HTTP client."""
    await self._client.aclose()

list_assignments async

list_assignments(
    filter: AssignmentFilter | None = None,
) -> list[Assignment]

List assignments, optionally filtered.

The Forecast API requires start_date and end_date parameters for the assignments endpoint. Large date ranges are automatically chunked into windows and results are deduplicated by id.

Parameters:

Name Type Description Default
filter AssignmentFilter | None

Filter criteria. Must include start_date and end_date.

None

Returns:

Type Description
list[Assignment]

List of Assignment objects.

Raises:

Type Description
ValueError

If filter is None or lacks start_date/end_date.

Source code in src/harvest_forecast/_async/client.py
async def list_assignments(self, filter: AssignmentFilter | None = None) -> list[Assignment]:
    """List assignments, optionally filtered.

    The Forecast API requires start_date and end_date parameters for the
    assignments endpoint. Large date ranges are automatically chunked
    into windows and results are deduplicated by id.

    Args:
        filter: Filter criteria. Must include start_date and end_date.

    Returns:
        List of Assignment objects.

    Raises:
        ValueError: If filter is None or lacks start_date/end_date.
    """
    if filter is None or filter.start_date is None or filter.end_date is None:
        raise ValueError("list_assignments requires start_date and end_date in the filter")
    params = filter.to_params()
    return [
        Assignment.model_validate(item)
        async for item in self._paginate_windowed(
            "/assignments",
            "assignments",
            filter.start_date,
            filter.end_date,
            params=params,
        )
    ]

list_clients async

list_clients() -> list[Client]

List all clients in the Forecast account.

Returns:

Type Description
list[Client]

List of Client objects.

Source code in src/harvest_forecast/_async/client.py
async def list_clients(self) -> list[Client]:
    """List all clients in the Forecast account.

    Returns:
        List of Client objects.
    """
    return [Client.model_validate(item) async for item in self._paginate("/clients", "clients")]

list_milestones async

list_milestones() -> list[Milestone]

List all milestones in the Forecast account.

Returns:

Type Description
list[Milestone]

List of Milestone objects.

Source code in src/harvest_forecast/_async/client.py
async def list_milestones(self) -> list[Milestone]:
    """List all milestones in the Forecast account.

    Returns:
        List of Milestone objects.
    """
    return [
        Milestone.model_validate(item)
        async for item in self._paginate("/milestones", "milestones")
    ]

list_people async

list_people() -> list[Person]

List all people being scheduled in Forecast.

Returns:

Type Description
list[Person]

List of Person objects.

Source code in src/harvest_forecast/_async/client.py
async def list_people(self) -> list[Person]:
    """List all people being scheduled in Forecast.

    Returns:
        List of Person objects.
    """
    return [Person.model_validate(item) async for item in self._paginate("/people", "people")]

list_placeholders async

list_placeholders() -> list[Placeholder]

List all placeholders being scheduled in Forecast.

Returns:

Type Description
list[Placeholder]

List of Placeholder objects.

Source code in src/harvest_forecast/_async/client.py
async def list_placeholders(self) -> list[Placeholder]:
    """List all placeholders being scheduled in Forecast.

    Returns:
        List of Placeholder objects.
    """
    return [
        Placeholder.model_validate(item)
        async for item in self._paginate("/placeholders", "placeholders")
    ]

list_projects async

list_projects() -> list[Project]

List all projects in the Forecast account.

Returns:

Type Description
list[Project]

List of Project objects.

Source code in src/harvest_forecast/_async/client.py
async def list_projects(self) -> list[Project]:
    """List all projects in the Forecast account.

    Returns:
        List of Project objects.
    """
    return [
        Project.model_validate(item) async for item in self._paginate("/projects", "projects")
    ]

list_roles async

list_roles() -> list[Role]

List all roles in Forecast.

Returns:

Type Description
list[Role]

List of Role objects.

Source code in src/harvest_forecast/_async/client.py
async def list_roles(self) -> list[Role]:
    """List all roles in Forecast.

    Returns:
        List of Role objects.
    """
    return [Role.model_validate(item) async for item in self._paginate("/roles", "roles")]

list_repeated_assignment_sets async

list_repeated_assignment_sets() -> list[
    RepeatedAssignmentSet
]

List all repeated assignment sets in the Forecast account.

Returns:

Type Description
list[RepeatedAssignmentSet]

List of RepeatedAssignmentSet objects.

Source code in src/harvest_forecast/_async/client.py
async def list_repeated_assignment_sets(self) -> list[RepeatedAssignmentSet]:
    """List all repeated assignment sets in the Forecast account.

    Returns:
        List of RepeatedAssignmentSet objects.
    """
    return [
        RepeatedAssignmentSet.model_validate(item)
        async for item in self._paginate(
            "/repeated_assignment_sets", "repeated_assignment_sets"
        )
    ]

list_user_connections async

list_user_connections() -> list[UserConnection]

List all current user connections.

Returns:

Type Description
list[UserConnection]

List of UserConnection objects.

Source code in src/harvest_forecast/_async/client.py
async def list_user_connections(self) -> list[UserConnection]:
    """List all current user connections.

    Returns:
        List of UserConnection objects.
    """
    return [
        UserConnection.model_validate(item)
        async for item in self._paginate("/user_connections", "user_connections")
    ]

get_person async

get_person(id: int) -> Person

Retrieve a single person by ID.

Parameters:

Name Type Description Default
id int

Person ID (must be >= 1).

required

Returns:

Type Description
Person

Person object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_async/client.py
async def get_person(self, id: int) -> Person:
    """Retrieve a single person by ID.

    Args:
        id: Person ID (must be >= 1).

    Returns:
        Person object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = await self._get(f"/people/{id}")
    return Person.model_validate(data["person"])

get_placeholder async

get_placeholder(id: int) -> Placeholder

Retrieve a single placeholder by ID.

Parameters:

Name Type Description Default
id int

Placeholder ID (must be >= 1).

required

Returns:

Type Description
Placeholder

Placeholder object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_async/client.py
async def get_placeholder(self, id: int) -> Placeholder:
    """Retrieve a single placeholder by ID.

    Args:
        id: Placeholder ID (must be >= 1).

    Returns:
        Placeholder object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = await self._get(f"/placeholders/{id}")
    return Placeholder.model_validate(data["placeholder"])

get_project async

get_project(id: int) -> Project

Retrieve a single project by ID.

Parameters:

Name Type Description Default
id int

Project ID (must be >= 1).

required

Returns:

Type Description
Project

Project object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_async/client.py
async def get_project(self, id: int) -> Project:
    """Retrieve a single project by ID.

    Args:
        id: Project ID (must be >= 1).

    Returns:
        Project object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = await self._get(f"/projects/{id}")
    return Project.model_validate(data["project"])

get_role async

get_role(id: int) -> Role

Retrieve a single role by ID.

Parameters:

Name Type Description Default
id int

Role ID (must be >= 1).

required

Returns:

Type Description
Role

Role object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_async/client.py
async def get_role(self, id: int) -> Role:
    """Retrieve a single role by ID.

    Args:
        id: Role ID (must be >= 1).

    Returns:
        Role object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = await self._get(f"/roles/{id}")
    return Role.model_validate(data["role"])

get_repeated_assignment_set async

get_repeated_assignment_set(
    id: int,
) -> RepeatedAssignmentSet

Retrieve a single repeated assignment set by ID.

Parameters:

Name Type Description Default
id int

Repeated assignment set ID (must be >= 1).

required

Returns:

Type Description
RepeatedAssignmentSet

RepeatedAssignmentSet object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_async/client.py
async def get_repeated_assignment_set(self, id: int) -> RepeatedAssignmentSet:
    """Retrieve a single repeated assignment set by ID.

    Args:
        id: Repeated assignment set ID (must be >= 1).

    Returns:
        RepeatedAssignmentSet object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = await self._get(f"/repeated_assignment_sets/{id}")
    return RepeatedAssignmentSet.model_validate(data["repeated_assignment_set"])

create_assignment async

create_assignment(req: AssignmentRequest) -> Assignment

Create a new assignment.

Parameters:

Name Type Description Default
req AssignmentRequest

Assignment creation request payload.

required

Returns:

Type Description
Assignment

The created Assignment object.

Raises:

Type Description
ValueError

If req.project_id < 1.

ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_async/client.py
async def create_assignment(self, req: AssignmentRequest) -> Assignment:
    """Create a new assignment.

    Args:
        req: Assignment creation request payload.

    Returns:
        The created Assignment object.

    Raises:
        ValueError: If req.project_id < 1.
        ForecastHTTPError: On HTTP errors.
    """
    if req.project_id < 1:
        raise ValueError(f"project_id must be >= 1, got {req.project_id}")
    data = await self._post("/assignments", req.to_payload())
    return Assignment.model_validate(data["assignment"])

update_assignment async

update_assignment(
    id: int, req: AssignmentRequest
) -> Assignment

Update an existing assignment.

Parameters:

Name Type Description Default
id int

Assignment ID (must be >= 1).

required
req AssignmentRequest

Assignment update request payload.

required

Returns:

Type Description
Assignment

The updated Assignment object.

Raises:

Type Description
ValueError

If id < 1 or req.project_id < 1.

ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_async/client.py
async def update_assignment(self, id: int, req: AssignmentRequest) -> Assignment:
    """Update an existing assignment.

    Args:
        id: Assignment ID (must be >= 1).
        req: Assignment update request payload.

    Returns:
        The updated Assignment object.

    Raises:
        ValueError: If id < 1 or req.project_id < 1.
        ForecastHTTPError: On HTTP errors.
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    if req.project_id < 1:
        raise ValueError(f"project_id must be >= 1, got {req.project_id}")
    data = await self._put(f"/assignments/{id}", req.to_payload())
    return Assignment.model_validate(data["assignment"])

delete_assignment async

delete_assignment(id: int) -> None

Delete an assignment.

Parameters:

Name Type Description Default
id int

Assignment ID (must be >= 1).

required

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_async/client.py
async def delete_assignment(self, id: int) -> None:
    """Delete an assignment.

    Args:
        id: Assignment ID (must be >= 1).

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors.
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    await self._delete(f"/assignments/{id}")

whoami async

whoami() -> CurrentUser

Retrieve the current authenticated user.

Returns:

Type Description
CurrentUser

CurrentUser object.

Source code in src/harvest_forecast/_async/client.py
async def whoami(self) -> CurrentUser:
    """Retrieve the current authenticated user.

    Returns:
        CurrentUser object.
    """
    data = await self._get("/whoami")
    return CurrentUser.model_validate(data["current_user"])

get_account async

get_account() -> Account

Retrieve the Forecast account metadata.

Returns:

Type Description
Account

Account object.

Source code in src/harvest_forecast/_async/client.py
async def get_account(self) -> Account:
    """Retrieve the Forecast account metadata.

    Returns:
        Account object.
    """
    data = await self._get(f"/accounts/{self._account_id}")
    return Account.model_validate(data["account"])

get_subscription async

get_subscription() -> Subscription

Retrieve the Forecast subscription details.

Returns:

Type Description
Subscription

Subscription object.

Source code in src/harvest_forecast/_async/client.py
async def get_subscription(self) -> Subscription:
    """Retrieve the Forecast subscription details.

    Returns:
        Subscription object.
    """
    data = await self._get("/billing/subscription")
    return Subscription.model_validate(data["subscription"])

remaining_budgeted_hours async

remaining_budgeted_hours() -> list[
    RemainingBudgetedHoursItem
]

Retrieve remaining budgeted hours for all projects.

Returns:

Type Description
list[RemainingBudgetedHoursItem]

List of RemainingBudgetedHoursItem objects.

Source code in src/harvest_forecast/_async/client.py
async def remaining_budgeted_hours(self) -> list[RemainingBudgetedHoursItem]:
    """Retrieve remaining budgeted hours for all projects.

    Returns:
        List of RemainingBudgetedHoursItem objects.
    """
    data = await self._get("/aggregate/remaining_budgeted_hours")
    return [
        RemainingBudgetedHoursItem.model_validate(item)
        for item in data["remaining_budgeted_hours"]
    ]

future_scheduled_hours async

future_scheduled_hours(
    from_date: str | date,
) -> list[FutureScheduledHoursItem]

Retrieve future scheduled hours starting from a date.

Parameters:

Name Type Description Default
from_date str | date

Starting date (ISO string or date object).

required

Returns:

Type Description
list[FutureScheduledHoursItem]

List of FutureScheduledHoursItem objects.

Source code in src/harvest_forecast/_async/client.py
async def future_scheduled_hours(self, from_date: str | date) -> list[FutureScheduledHoursItem]:
    """Retrieve future scheduled hours starting from a date.

    Args:
        from_date: Starting date (ISO string or date object).

    Returns:
        List of FutureScheduledHoursItem objects.
    """
    from_str = from_date.isoformat() if isinstance(from_date, date) else from_date
    data = await self._get(f"/aggregate/future_scheduled_hours/{from_str}")
    return [
        FutureScheduledHoursItem.model_validate(item) for item in data["future_scheduled_hours"]
    ]

future_scheduled_hours_for_project async

future_scheduled_hours_for_project(
    from_date: str | date, project_id: int
) -> list[FutureScheduledHoursItem]

Retrieve future scheduled hours for a specific project.

Parameters:

Name Type Description Default
from_date str | date

Starting date (ISO string or date object).

required
project_id int

Project ID to filter by.

required

Returns:

Type Description
list[FutureScheduledHoursItem]

List of FutureScheduledHoursItem objects.

Source code in src/harvest_forecast/_async/client.py
async def future_scheduled_hours_for_project(
    self, from_date: str | date, project_id: int
) -> list[FutureScheduledHoursItem]:
    """Retrieve future scheduled hours for a specific project.

    Args:
        from_date: Starting date (ISO string or date object).
        project_id: Project ID to filter by.

    Returns:
        List of FutureScheduledHoursItem objects.
    """
    from_str = from_date.isoformat() if isinstance(from_date, date) else from_date
    data = await self._get(
        f"/aggregate/future_scheduled_hours/{from_str}",
        params={"project_id": str(project_id)},
    )
    return [
        FutureScheduledHoursItem.model_validate(item) for item in data["future_scheduled_hours"]
    ]

assigned_people async

assigned_people(
    start_date: str | date, end_date: str | date
) -> dict[str, list[int]]

Retrieve a mapping of project IDs to assigned person IDs.

Parameters:

Name Type Description Default
start_date str | date

Start date (ISO string or date object).

required
end_date str | date

End date (ISO string or date object).

required

Returns:

Type Description
dict[str, list[int]]

Dict mapping project ID strings to lists of person IDs.

Source code in src/harvest_forecast/_async/client.py
async def assigned_people(
    self, start_date: str | date, end_date: str | date
) -> dict[str, list[int]]:
    """Retrieve a mapping of project IDs to assigned person IDs.

    Args:
        start_date: Start date (ISO string or date object).
        end_date: End date (ISO string or date object).

    Returns:
        Dict mapping project ID strings to lists of person IDs.
    """
    start_str = start_date.isoformat() if isinstance(start_date, date) else start_date
    end_str = end_date.isoformat() if isinstance(end_date, date) else end_date
    data = await self._get(
        "/aggregate/projects/assigned_people",
        params={"start_date": start_str, "end_date": end_str},
    )
    return {str(k): list(v) for k, v in data.items()}

project_heatmap async

project_heatmap(
    from_: str | date,
    to: str | date,
    project_id: int,
    scale: str = "daily",
) -> list[ProjectHeatmapItem]

Retrieve a project heatmap for a time period.

Parameters:

Name Type Description Default
from_ str | date

Start date (ISO string or date object).

required
to str | date

End date (ISO string or date object).

required
project_id int

Project ID.

required
scale str

Time scale (e.g. "daily", "weekly").

'daily'

Returns:

Type Description
list[ProjectHeatmapItem]

List of ProjectHeatmapItem objects.

Source code in src/harvest_forecast/_async/client.py
async def project_heatmap(
    self, from_: str | date, to: str | date, project_id: int, scale: str = "daily"
) -> list[ProjectHeatmapItem]:
    """Retrieve a project heatmap for a time period.

    Args:
        from_: Start date (ISO string or date object).
        to: End date (ISO string or date object).
        project_id: Project ID.
        scale: Time scale (e.g. "daily", "weekly").

    Returns:
        List of ProjectHeatmapItem objects.
    """
    from_str = from_.isoformat() if isinstance(from_, date) else from_
    to_str = to.isoformat() if isinstance(to, date) else to
    params = {
        "starting": from_str,
        "ending": to_str,
        "project_id": str(project_id),
        "scale": scale,
    }
    data = await self._get("/aggregate/heatmap/project", params=params)
    items = cast("list[dict[str, Any]]", data)
    return [ProjectHeatmapItem.model_validate(item) for item in items]

person_heatmap async

person_heatmap(
    from_: str | date,
    to: str | date,
    person_id: int,
    scale: str = "daily",
) -> list[PersonHeatmapItem]

Retrieve a person heatmap for a time period.

Parameters:

Name Type Description Default
from_ str | date

Start date (ISO string or date object).

required
to str | date

End date (ISO string or date object).

required
person_id int

Person ID.

required
scale str

Time scale (e.g. "daily", "weekly").

'daily'

Returns:

Type Description
list[PersonHeatmapItem]

List of PersonHeatmapItem objects.

Source code in src/harvest_forecast/_async/client.py
async def person_heatmap(
    self, from_: str | date, to: str | date, person_id: int, scale: str = "daily"
) -> list[PersonHeatmapItem]:
    """Retrieve a person heatmap for a time period.

    Args:
        from_: Start date (ISO string or date object).
        to: End date (ISO string or date object).
        person_id: Person ID.
        scale: Time scale (e.g. "daily", "weekly").

    Returns:
        List of PersonHeatmapItem objects.
    """
    from_str = from_.isoformat() if isinstance(from_, date) else from_
    to_str = to.isoformat() if isinstance(to, date) else to
    params = {
        "starting": from_str,
        "ending": to_str,
        "person_id": str(person_id),
        "scale": scale,
    }
    data = await self._get("/aggregate/heatmap/person", params=params)
    items = cast("list[dict[str, Any]]", data)
    return [PersonHeatmapItem.model_validate(item) for item in items]

placeholder_heatmap async

placeholder_heatmap(
    from_: str | date,
    to: str | date,
    placeholder_id: int,
    scale: str = "daily",
) -> list[PlaceholderHeatmapItem]

Retrieve a placeholder heatmap for a time period.

Parameters:

Name Type Description Default
from_ str | date

Start date (ISO string or date object).

required
to str | date

End date (ISO string or date object).

required
placeholder_id int

Placeholder ID.

required
scale str

Time scale (e.g. "daily", "weekly").

'daily'

Returns:

Type Description
list[PlaceholderHeatmapItem]

List of PlaceholderHeatmapItem objects.

Source code in src/harvest_forecast/_async/client.py
async def placeholder_heatmap(
    self, from_: str | date, to: str | date, placeholder_id: int, scale: str = "daily"
) -> list[PlaceholderHeatmapItem]:
    """Retrieve a placeholder heatmap for a time period.

    Args:
        from_: Start date (ISO string or date object).
        to: End date (ISO string or date object).
        placeholder_id: Placeholder ID.
        scale: Time scale (e.g. "daily", "weekly").

    Returns:
        List of PlaceholderHeatmapItem objects.
    """
    from_str = from_.isoformat() if isinstance(from_, date) else from_
    to_str = to.isoformat() if isinstance(to, date) else to
    params = {
        "starting": from_str,
        "ending": to_str,
        "placeholder_id": str(placeholder_id),
        "scale": scale,
    }
    data = await self._get("/aggregate/heatmap/placeholder", params=params)
    items = cast("list[dict[str, Any]]", data)
    return [PlaceholderHeatmapItem.model_validate(item) for item in items]

Sync client

SyncForecastClient

SyncForecastClient(
    access_token: str,
    account_id: str,
    user_agent: str,
    *,
    base_url: str = "https://api.forecastapp.com",
    timeout: float = 30.0,
    retry: RetryPolicy | None = None,
)

Async client for the Harvest Forecast API.

Example

async with AsyncForecastClient( access_token="token", account_id="123", user_agent="my-app (you@example.com)", ) as client: people = await client.list_people()

Initialize the async Forecast client.

Parameters:

Name Type Description Default
access_token str

Forecast personal access token.

required
account_id str

Forecast account ID.

required
user_agent str

User-Agent header value sent with every request.

required
base_url str

Forecast API base URL.

'https://api.forecastapp.com'
timeout float

Request timeout in seconds.

30.0
retry RetryPolicy | None

Retry policy for transient failures.

None
Source code in src/harvest_forecast/_sync/client.py
def __init__(
    self,
    access_token: str,
    account_id: str,
    user_agent: str,
    *,
    base_url: str = "https://api.forecastapp.com",
    timeout: float = 30.0,
    retry: RetryPolicy | None = None,
) -> None:
    """Initialize the async Forecast client.

    Args:
        access_token: Forecast personal access token.
        account_id: Forecast account ID.
        user_agent: User-Agent header value sent with every request.
        base_url: Forecast API base URL.
        timeout: Request timeout in seconds.
        retry: Retry policy for transient failures.
    """
    self._retry = retry or RetryPolicy()
    self._account_id = account_id
    self._client = httpx.Client(
        base_url=base_url,
        timeout=httpx.Timeout(timeout, connect=10.0),
        headers={
            "Authorization": f"Bearer {access_token}",
            "Forecast-Account-ID": account_id,
            "User-Agent": user_agent,
            "Accept": "application/json",
        },
        limits=httpx.Limits(max_connections=8, max_keepalive_connections=4),
        follow_redirects=False,
    )

__enter__

__enter__() -> Self

Enter the async context manager.

Source code in src/harvest_forecast/_sync/client.py
def __enter__(self) -> Self:
    """Enter the async context manager."""
    return self

__exit__

__exit__(*_: object) -> None

Exit the async context manager, closing the HTTP client.

Source code in src/harvest_forecast/_sync/client.py
def __exit__(self, *_: object) -> None:
    """Exit the async context manager, closing the HTTP client."""
    self.close()

close

close() -> None

Close the underlying HTTP client.

Source code in src/harvest_forecast/_sync/client.py
def close(self) -> None:
    """Close the underlying HTTP client."""
    self._client.close()

list_assignments

list_assignments(
    filter: AssignmentFilter | None = None,
) -> list[Assignment]

List assignments, optionally filtered.

The Forecast API requires start_date and end_date parameters for the assignments endpoint. Large date ranges are automatically chunked into windows and results are deduplicated by id.

Parameters:

Name Type Description Default
filter AssignmentFilter | None

Filter criteria. Must include start_date and end_date.

None

Returns:

Type Description
list[Assignment]

List of Assignment objects.

Raises:

Type Description
ValueError

If filter is None or lacks start_date/end_date.

Source code in src/harvest_forecast/_sync/client.py
def list_assignments(self, filter: AssignmentFilter | None = None) -> list[Assignment]:
    """List assignments, optionally filtered.

    The Forecast API requires start_date and end_date parameters for the
    assignments endpoint. Large date ranges are automatically chunked
    into windows and results are deduplicated by id.

    Args:
        filter: Filter criteria. Must include start_date and end_date.

    Returns:
        List of Assignment objects.

    Raises:
        ValueError: If filter is None or lacks start_date/end_date.
    """
    if filter is None or filter.start_date is None or filter.end_date is None:
        raise ValueError("list_assignments requires start_date and end_date in the filter")
    params = filter.to_params()
    return [
        Assignment.model_validate(item)
        for item in self._paginate_windowed(
            "/assignments",
            "assignments",
            filter.start_date,
            filter.end_date,
            params=params,
        )
    ]

list_clients

list_clients() -> list[Client]

List all clients in the Forecast account.

Returns:

Type Description
list[Client]

List of Client objects.

Source code in src/harvest_forecast/_sync/client.py
def list_clients(self) -> list[Client]:
    """List all clients in the Forecast account.

    Returns:
        List of Client objects.
    """
    return [Client.model_validate(item) for item in self._paginate("/clients", "clients")]

list_milestones

list_milestones() -> list[Milestone]

List all milestones in the Forecast account.

Returns:

Type Description
list[Milestone]

List of Milestone objects.

Source code in src/harvest_forecast/_sync/client.py
def list_milestones(self) -> list[Milestone]:
    """List all milestones in the Forecast account.

    Returns:
        List of Milestone objects.
    """
    return [
        Milestone.model_validate(item)
        for item in self._paginate("/milestones", "milestones")
    ]

list_people

list_people() -> list[Person]

List all people being scheduled in Forecast.

Returns:

Type Description
list[Person]

List of Person objects.

Source code in src/harvest_forecast/_sync/client.py
def list_people(self) -> list[Person]:
    """List all people being scheduled in Forecast.

    Returns:
        List of Person objects.
    """
    return [Person.model_validate(item) for item in self._paginate("/people", "people")]

list_placeholders

list_placeholders() -> list[Placeholder]

List all placeholders being scheduled in Forecast.

Returns:

Type Description
list[Placeholder]

List of Placeholder objects.

Source code in src/harvest_forecast/_sync/client.py
def list_placeholders(self) -> list[Placeholder]:
    """List all placeholders being scheduled in Forecast.

    Returns:
        List of Placeholder objects.
    """
    return [
        Placeholder.model_validate(item)
        for item in self._paginate("/placeholders", "placeholders")
    ]

list_projects

list_projects() -> list[Project]

List all projects in the Forecast account.

Returns:

Type Description
list[Project]

List of Project objects.

Source code in src/harvest_forecast/_sync/client.py
def list_projects(self) -> list[Project]:
    """List all projects in the Forecast account.

    Returns:
        List of Project objects.
    """
    return [
        Project.model_validate(item) for item in self._paginate("/projects", "projects")
    ]

list_roles

list_roles() -> list[Role]

List all roles in Forecast.

Returns:

Type Description
list[Role]

List of Role objects.

Source code in src/harvest_forecast/_sync/client.py
def list_roles(self) -> list[Role]:
    """List all roles in Forecast.

    Returns:
        List of Role objects.
    """
    return [Role.model_validate(item) for item in self._paginate("/roles", "roles")]

list_repeated_assignment_sets

list_repeated_assignment_sets() -> list[
    RepeatedAssignmentSet
]

List all repeated assignment sets in the Forecast account.

Returns:

Type Description
list[RepeatedAssignmentSet]

List of RepeatedAssignmentSet objects.

Source code in src/harvest_forecast/_sync/client.py
def list_repeated_assignment_sets(self) -> list[RepeatedAssignmentSet]:
    """List all repeated assignment sets in the Forecast account.

    Returns:
        List of RepeatedAssignmentSet objects.
    """
    return [
        RepeatedAssignmentSet.model_validate(item)
        for item in self._paginate(
            "/repeated_assignment_sets", "repeated_assignment_sets"
        )
    ]

list_user_connections

list_user_connections() -> list[UserConnection]

List all current user connections.

Returns:

Type Description
list[UserConnection]

List of UserConnection objects.

Source code in src/harvest_forecast/_sync/client.py
def list_user_connections(self) -> list[UserConnection]:
    """List all current user connections.

    Returns:
        List of UserConnection objects.
    """
    return [
        UserConnection.model_validate(item)
        for item in self._paginate("/user_connections", "user_connections")
    ]

get_person

get_person(id: int) -> Person

Retrieve a single person by ID.

Parameters:

Name Type Description Default
id int

Person ID (must be >= 1).

required

Returns:

Type Description
Person

Person object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_sync/client.py
def get_person(self, id: int) -> Person:
    """Retrieve a single person by ID.

    Args:
        id: Person ID (must be >= 1).

    Returns:
        Person object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = self._get(f"/people/{id}")
    return Person.model_validate(data["person"])

get_placeholder

get_placeholder(id: int) -> Placeholder

Retrieve a single placeholder by ID.

Parameters:

Name Type Description Default
id int

Placeholder ID (must be >= 1).

required

Returns:

Type Description
Placeholder

Placeholder object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_sync/client.py
def get_placeholder(self, id: int) -> Placeholder:
    """Retrieve a single placeholder by ID.

    Args:
        id: Placeholder ID (must be >= 1).

    Returns:
        Placeholder object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = self._get(f"/placeholders/{id}")
    return Placeholder.model_validate(data["placeholder"])

get_project

get_project(id: int) -> Project

Retrieve a single project by ID.

Parameters:

Name Type Description Default
id int

Project ID (must be >= 1).

required

Returns:

Type Description
Project

Project object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_sync/client.py
def get_project(self, id: int) -> Project:
    """Retrieve a single project by ID.

    Args:
        id: Project ID (must be >= 1).

    Returns:
        Project object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = self._get(f"/projects/{id}")
    return Project.model_validate(data["project"])

get_role

get_role(id: int) -> Role

Retrieve a single role by ID.

Parameters:

Name Type Description Default
id int

Role ID (must be >= 1).

required

Returns:

Type Description
Role

Role object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_sync/client.py
def get_role(self, id: int) -> Role:
    """Retrieve a single role by ID.

    Args:
        id: Role ID (must be >= 1).

    Returns:
        Role object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = self._get(f"/roles/{id}")
    return Role.model_validate(data["role"])

get_repeated_assignment_set

get_repeated_assignment_set(
    id: int,
) -> RepeatedAssignmentSet

Retrieve a single repeated assignment set by ID.

Parameters:

Name Type Description Default
id int

Repeated assignment set ID (must be >= 1).

required

Returns:

Type Description
RepeatedAssignmentSet

RepeatedAssignmentSet object.

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors (e.g. 404).

Source code in src/harvest_forecast/_sync/client.py
def get_repeated_assignment_set(self, id: int) -> RepeatedAssignmentSet:
    """Retrieve a single repeated assignment set by ID.

    Args:
        id: Repeated assignment set ID (must be >= 1).

    Returns:
        RepeatedAssignmentSet object.

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors (e.g. 404).
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    data = self._get(f"/repeated_assignment_sets/{id}")
    return RepeatedAssignmentSet.model_validate(data["repeated_assignment_set"])

create_assignment

create_assignment(req: AssignmentRequest) -> Assignment

Create a new assignment.

Parameters:

Name Type Description Default
req AssignmentRequest

Assignment creation request payload.

required

Returns:

Type Description
Assignment

The created Assignment object.

Raises:

Type Description
ValueError

If req.project_id < 1.

ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_sync/client.py
def create_assignment(self, req: AssignmentRequest) -> Assignment:
    """Create a new assignment.

    Args:
        req: Assignment creation request payload.

    Returns:
        The created Assignment object.

    Raises:
        ValueError: If req.project_id < 1.
        ForecastHTTPError: On HTTP errors.
    """
    if req.project_id < 1:
        raise ValueError(f"project_id must be >= 1, got {req.project_id}")
    data = self._post("/assignments", req.to_payload())
    return Assignment.model_validate(data["assignment"])

update_assignment

update_assignment(
    id: int, req: AssignmentRequest
) -> Assignment

Update an existing assignment.

Parameters:

Name Type Description Default
id int

Assignment ID (must be >= 1).

required
req AssignmentRequest

Assignment update request payload.

required

Returns:

Type Description
Assignment

The updated Assignment object.

Raises:

Type Description
ValueError

If id < 1 or req.project_id < 1.

ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_sync/client.py
def update_assignment(self, id: int, req: AssignmentRequest) -> Assignment:
    """Update an existing assignment.

    Args:
        id: Assignment ID (must be >= 1).
        req: Assignment update request payload.

    Returns:
        The updated Assignment object.

    Raises:
        ValueError: If id < 1 or req.project_id < 1.
        ForecastHTTPError: On HTTP errors.
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    if req.project_id < 1:
        raise ValueError(f"project_id must be >= 1, got {req.project_id}")
    data = self._put(f"/assignments/{id}", req.to_payload())
    return Assignment.model_validate(data["assignment"])

delete_assignment

delete_assignment(id: int) -> None

Delete an assignment.

Parameters:

Name Type Description Default
id int

Assignment ID (must be >= 1).

required

Raises:

Type Description
ValueError

If id < 1.

ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_sync/client.py
def delete_assignment(self, id: int) -> None:
    """Delete an assignment.

    Args:
        id: Assignment ID (must be >= 1).

    Raises:
        ValueError: If id < 1.
        ForecastHTTPError: On HTTP errors.
    """
    if id < 1:
        raise ValueError(f"id must be >= 1, got {id}")
    self._delete(f"/assignments/{id}")

whoami

whoami() -> CurrentUser

Retrieve the current authenticated user.

Returns:

Type Description
CurrentUser

CurrentUser object.

Source code in src/harvest_forecast/_sync/client.py
def whoami(self) -> CurrentUser:
    """Retrieve the current authenticated user.

    Returns:
        CurrentUser object.
    """
    data = self._get("/whoami")
    return CurrentUser.model_validate(data["current_user"])

get_account

get_account() -> Account

Retrieve the Forecast account metadata.

Returns:

Type Description
Account

Account object.

Source code in src/harvest_forecast/_sync/client.py
def get_account(self) -> Account:
    """Retrieve the Forecast account metadata.

    Returns:
        Account object.
    """
    data = self._get(f"/accounts/{self._account_id}")
    return Account.model_validate(data["account"])

get_subscription

get_subscription() -> Subscription

Retrieve the Forecast subscription details.

Returns:

Type Description
Subscription

Subscription object.

Source code in src/harvest_forecast/_sync/client.py
def get_subscription(self) -> Subscription:
    """Retrieve the Forecast subscription details.

    Returns:
        Subscription object.
    """
    data = self._get("/billing/subscription")
    return Subscription.model_validate(data["subscription"])

remaining_budgeted_hours

remaining_budgeted_hours() -> list[
    RemainingBudgetedHoursItem
]

Retrieve remaining budgeted hours for all projects.

Returns:

Type Description
list[RemainingBudgetedHoursItem]

List of RemainingBudgetedHoursItem objects.

Source code in src/harvest_forecast/_sync/client.py
def remaining_budgeted_hours(self) -> list[RemainingBudgetedHoursItem]:
    """Retrieve remaining budgeted hours for all projects.

    Returns:
        List of RemainingBudgetedHoursItem objects.
    """
    data = self._get("/aggregate/remaining_budgeted_hours")
    return [
        RemainingBudgetedHoursItem.model_validate(item)
        for item in data["remaining_budgeted_hours"]
    ]

future_scheduled_hours

future_scheduled_hours(
    from_date: str | date,
) -> list[FutureScheduledHoursItem]

Retrieve future scheduled hours starting from a date.

Parameters:

Name Type Description Default
from_date str | date

Starting date (ISO string or date object).

required

Returns:

Type Description
list[FutureScheduledHoursItem]

List of FutureScheduledHoursItem objects.

Source code in src/harvest_forecast/_sync/client.py
def future_scheduled_hours(self, from_date: str | date) -> list[FutureScheduledHoursItem]:
    """Retrieve future scheduled hours starting from a date.

    Args:
        from_date: Starting date (ISO string or date object).

    Returns:
        List of FutureScheduledHoursItem objects.
    """
    from_str = from_date.isoformat() if isinstance(from_date, date) else from_date
    data = self._get(f"/aggregate/future_scheduled_hours/{from_str}")
    return [
        FutureScheduledHoursItem.model_validate(item) for item in data["future_scheduled_hours"]
    ]

future_scheduled_hours_for_project

future_scheduled_hours_for_project(
    from_date: str | date, project_id: int
) -> list[FutureScheduledHoursItem]

Retrieve future scheduled hours for a specific project.

Parameters:

Name Type Description Default
from_date str | date

Starting date (ISO string or date object).

required
project_id int

Project ID to filter by.

required

Returns:

Type Description
list[FutureScheduledHoursItem]

List of FutureScheduledHoursItem objects.

Source code in src/harvest_forecast/_sync/client.py
def future_scheduled_hours_for_project(
    self, from_date: str | date, project_id: int
) -> list[FutureScheduledHoursItem]:
    """Retrieve future scheduled hours for a specific project.

    Args:
        from_date: Starting date (ISO string or date object).
        project_id: Project ID to filter by.

    Returns:
        List of FutureScheduledHoursItem objects.
    """
    from_str = from_date.isoformat() if isinstance(from_date, date) else from_date
    data = self._get(
        f"/aggregate/future_scheduled_hours/{from_str}",
        params={"project_id": str(project_id)},
    )
    return [
        FutureScheduledHoursItem.model_validate(item) for item in data["future_scheduled_hours"]
    ]

assigned_people

assigned_people(
    start_date: str | date, end_date: str | date
) -> dict[str, list[int]]

Retrieve a mapping of project IDs to assigned person IDs.

Parameters:

Name Type Description Default
start_date str | date

Start date (ISO string or date object).

required
end_date str | date

End date (ISO string or date object).

required

Returns:

Type Description
dict[str, list[int]]

Dict mapping project ID strings to lists of person IDs.

Source code in src/harvest_forecast/_sync/client.py
def assigned_people(
    self, start_date: str | date, end_date: str | date
) -> dict[str, list[int]]:
    """Retrieve a mapping of project IDs to assigned person IDs.

    Args:
        start_date: Start date (ISO string or date object).
        end_date: End date (ISO string or date object).

    Returns:
        Dict mapping project ID strings to lists of person IDs.
    """
    start_str = start_date.isoformat() if isinstance(start_date, date) else start_date
    end_str = end_date.isoformat() if isinstance(end_date, date) else end_date
    data = self._get(
        "/aggregate/projects/assigned_people",
        params={"start_date": start_str, "end_date": end_str},
    )
    return {str(k): list(v) for k, v in data.items()}

project_heatmap

project_heatmap(
    from_: str | date,
    to: str | date,
    project_id: int,
    scale: str = "daily",
) -> list[ProjectHeatmapItem]

Retrieve a project heatmap for a time period.

Parameters:

Name Type Description Default
from_ str | date

Start date (ISO string or date object).

required
to str | date

End date (ISO string or date object).

required
project_id int

Project ID.

required
scale str

Time scale (e.g. "daily", "weekly").

'daily'

Returns:

Type Description
list[ProjectHeatmapItem]

List of ProjectHeatmapItem objects.

Source code in src/harvest_forecast/_sync/client.py
def project_heatmap(
    self, from_: str | date, to: str | date, project_id: int, scale: str = "daily"
) -> list[ProjectHeatmapItem]:
    """Retrieve a project heatmap for a time period.

    Args:
        from_: Start date (ISO string or date object).
        to: End date (ISO string or date object).
        project_id: Project ID.
        scale: Time scale (e.g. "daily", "weekly").

    Returns:
        List of ProjectHeatmapItem objects.
    """
    from_str = from_.isoformat() if isinstance(from_, date) else from_
    to_str = to.isoformat() if isinstance(to, date) else to
    params = {
        "starting": from_str,
        "ending": to_str,
        "project_id": str(project_id),
        "scale": scale,
    }
    data = self._get("/aggregate/heatmap/project", params=params)
    items = cast("list[dict[str, Any]]", data)
    return [ProjectHeatmapItem.model_validate(item) for item in items]

person_heatmap

person_heatmap(
    from_: str | date,
    to: str | date,
    person_id: int,
    scale: str = "daily",
) -> list[PersonHeatmapItem]

Retrieve a person heatmap for a time period.

Parameters:

Name Type Description Default
from_ str | date

Start date (ISO string or date object).

required
to str | date

End date (ISO string or date object).

required
person_id int

Person ID.

required
scale str

Time scale (e.g. "daily", "weekly").

'daily'

Returns:

Type Description
list[PersonHeatmapItem]

List of PersonHeatmapItem objects.

Source code in src/harvest_forecast/_sync/client.py
def person_heatmap(
    self, from_: str | date, to: str | date, person_id: int, scale: str = "daily"
) -> list[PersonHeatmapItem]:
    """Retrieve a person heatmap for a time period.

    Args:
        from_: Start date (ISO string or date object).
        to: End date (ISO string or date object).
        person_id: Person ID.
        scale: Time scale (e.g. "daily", "weekly").

    Returns:
        List of PersonHeatmapItem objects.
    """
    from_str = from_.isoformat() if isinstance(from_, date) else from_
    to_str = to.isoformat() if isinstance(to, date) else to
    params = {
        "starting": from_str,
        "ending": to_str,
        "person_id": str(person_id),
        "scale": scale,
    }
    data = self._get("/aggregate/heatmap/person", params=params)
    items = cast("list[dict[str, Any]]", data)
    return [PersonHeatmapItem.model_validate(item) for item in items]

placeholder_heatmap

placeholder_heatmap(
    from_: str | date,
    to: str | date,
    placeholder_id: int,
    scale: str = "daily",
) -> list[PlaceholderHeatmapItem]

Retrieve a placeholder heatmap for a time period.

Parameters:

Name Type Description Default
from_ str | date

Start date (ISO string or date object).

required
to str | date

End date (ISO string or date object).

required
placeholder_id int

Placeholder ID.

required
scale str

Time scale (e.g. "daily", "weekly").

'daily'

Returns:

Type Description
list[PlaceholderHeatmapItem]

List of PlaceholderHeatmapItem objects.

Source code in src/harvest_forecast/_sync/client.py
def placeholder_heatmap(
    self, from_: str | date, to: str | date, placeholder_id: int, scale: str = "daily"
) -> list[PlaceholderHeatmapItem]:
    """Retrieve a placeholder heatmap for a time period.

    Args:
        from_: Start date (ISO string or date object).
        to: End date (ISO string or date object).
        placeholder_id: Placeholder ID.
        scale: Time scale (e.g. "daily", "weekly").

    Returns:
        List of PlaceholderHeatmapItem objects.
    """
    from_str = from_.isoformat() if isinstance(from_, date) else from_
    to_str = to.isoformat() if isinstance(to, date) else to
    params = {
        "starting": from_str,
        "ending": to_str,
        "placeholder_id": str(placeholder_id),
        "scale": scale,
    }
    data = self._get("/aggregate/heatmap/placeholder", params=params)
    items = cast("list[dict[str, Any]]", data)
    return [PlaceholderHeatmapItem.model_validate(item) for item in items]