Skip to content

Package Overview

The harvest_forecast package provides a fully-typed Python client for the Harvest Forecast API, with both async and sync clients, automatic retry, pagination, and date-windowing.

Exports

The package exports the following public API:

Clients:

Schemas:

Exceptions:

Retry:

Module Reference

harvest_forecast

harvest_forecast — Python clients for the Harvest Forecast and Harvest APIs.

Canonical imports:

from harvest_forecast import ForecastClient, SyncForecastClient
from harvest_forecast import HarvestClient, SyncHarvestClient
from harvest_forecast import AssignmentFilter, AssignmentRequest
from harvest_forecast import RetryPolicy
from harvest_forecast import ForecastError, ForecastHTTPError

__version__ module-attribute

__version__ = '0.2.0'

__author__ module-attribute

__author__ = 'AZX, PBC.'

__email__ module-attribute

__email__ = 'oss@azx.io'

__license__ module-attribute

__license__ = 'MIT'

__url__ module-attribute

__url__ = (
    "https://github.com/AZX-PBC-OSS/harvest-forecast-py"
)

__all__ module-attribute

__all__ = [
    "Account",
    "Assignment",
    "AssignmentFilter",
    "AssignmentRequest",
    "Client",
    "ColorLabel",
    "CurrentUser",
    "ForecastAuthError",
    "ForecastClient",
    "ForecastError",
    "ForecastHTTPError",
    "ForecastModel",
    "ForecastNotFoundError",
    "ForecastRateLimitError",
    "ForecastServerError",
    "ForecastValidationError",
    "FutureScheduledHoursItem",
    "HarvestClient",
    "HarvestClientRef",
    "HarvestCurrentUser",
    "HarvestProject",
    "HarvestProjectRef",
    "HarvestTask",
    "HarvestTaskRef",
    "HarvestTimeEntry",
    "HarvestUser",
    "HarvestUserAssignment",
    "HarvestUserRef",
    "Milestone",
    "Person",
    "PersonHeatmapItem",
    "Placeholder",
    "PlaceholderHeatmapItem",
    "Project",
    "ProjectHeatmapItem",
    "RemainingBudgetedHoursItem",
    "RepeatedAssignmentSet",
    "RetryPolicy",
    "Role",
    "Subscription",
    "SubscriptionAddress",
    "SubscriptionCard",
    "SubscriptionDiscounts",
    "SubscriptionIntervalUnitAmounts",
    "SyncForecastClient",
    "SyncHarvestClient",
    "UserConnection",
    "WorkingDays",
    "__version__",
]

ForecastClient

ForecastClient(
    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]

SyncHarvestClient

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

Sync client for the Harvest API v2.

Example

with SyncHarvestClient( access_token="token", account_id="123", user_agent="my-app (you@example.com)", ) as client: projects = client.list_projects()

Initialize the sync Harvest client.

Parameters:

Name Type Description Default
access_token str

Harvest personal access token.

required
account_id str

Harvest account ID.

required
user_agent str

User-Agent header value sent with every request.

required
base_url str

Harvest API base URL.

'https://api.harvestapp.com/v2'
timeout float

Request timeout in seconds.

30.0
retry RetryPolicy | None

Retry policy for transient failures.

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

    Args:
        access_token: Harvest personal access token.
        account_id: Harvest account ID.
        user_agent: User-Agent header value sent with every request.
        base_url: Harvest API base URL.
        timeout: Request timeout in seconds.
        retry: Retry policy for transient failures.
    """
    self._retry = retry or RetryPolicy()
    self._client = httpx.Client(
        base_url=base_url,
        timeout=httpx.Timeout(timeout, connect=10.0),
        headers={
            "Authorization": f"Bearer {access_token}",
            "Harvest-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 context manager.

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

__exit__

__exit__(*_: object) -> None

Exit the context manager, closing the HTTP client.

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

close

close() -> None

Close the underlying HTTP client.

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

list_projects

list_projects(
    *,
    is_active: bool | None = None,
    client_id: int | None = None,
) -> list[HarvestProject]

List all projects in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None
client_id int | None

Filter by client ID.

None

Returns:

Type Description
list[HarvestProject]

List of HarvestProject objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_projects(
    self, *, is_active: bool | None = None, client_id: int | None = None
) -> list[HarvestProject]:
    """List all projects in the Harvest account.

    Args:
        is_active: Filter by active status.
        client_id: Filter by client ID.

    Returns:
        List of HarvestProject objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    if client_id is not None:
        params["client_id"] = str(client_id)
    return [
        HarvestProject.model_validate(item)
        for item in self._paginate("/projects", "projects", params=params)
    ]

list_users

list_users(
    *, is_active: bool | None = None
) -> list[HarvestUser]

List all users in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None

Returns:

Type Description
list[HarvestUser]

List of HarvestUser objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_users(self, *, is_active: bool | None = None) -> list[HarvestUser]:
    """List all users in the Harvest account.

    Args:
        is_active: Filter by active status.

    Returns:
        List of HarvestUser objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    return [
        HarvestUser.model_validate(item)
        for item in self._paginate("/users", "users", params=params)
    ]

list_clients

list_clients(
    *, is_active: bool | None = None
) -> list[HarvestClient]

List all clients in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None

Returns:

Type Description
list[HarvestClient]

List of HarvestClient objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_clients(self, *, is_active: bool | None = None) -> list[HarvestClient]:
    """List all clients in the Harvest account.

    Args:
        is_active: Filter by active status.

    Returns:
        List of HarvestClient objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    return [
        HarvestClient.model_validate(item)
        for item in self._paginate("/clients", "clients", params=params)
    ]

list_tasks

list_tasks(
    *, is_active: bool | None = None
) -> list[HarvestTask]

List all tasks in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None

Returns:

Type Description
list[HarvestTask]

List of HarvestTask objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_tasks(self, *, is_active: bool | None = None) -> list[HarvestTask]:
    """List all tasks in the Harvest account.

    Args:
        is_active: Filter by active status.

    Returns:
        List of HarvestTask objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    return [
        HarvestTask.model_validate(item)
        for item in self._paginate("/tasks", "tasks", params=params)
    ]

list_time_entries

list_time_entries(
    *,
    user_id: int | None = None,
    project_id: int | None = None,
    from_date: str | date | None = None,
    to_date: str | date | None = None,
) -> list[HarvestTimeEntry]

List time entries, optionally filtered.

Parameters:

Name Type Description Default
user_id int | None

Filter by user ID.

None
project_id int | None

Filter by project ID.

None
from_date str | date | None

Start date (ISO string or date object).

None
to_date str | date | None

End date (ISO string or date object).

None

Returns:

Type Description
list[HarvestTimeEntry]

List of HarvestTimeEntry objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_time_entries(
    self,
    *,
    user_id: int | None = None,
    project_id: int | None = None,
    from_date: str | date | None = None,
    to_date: str | date | None = None,
) -> list[HarvestTimeEntry]:
    """List time entries, optionally filtered.

    Args:
        user_id: Filter by user ID.
        project_id: Filter by project ID.
        from_date: Start date (ISO string or date object).
        to_date: End date (ISO string or date object).

    Returns:
        List of HarvestTimeEntry objects.
    """
    params: dict[str, str] = {}
    if user_id is not None:
        params["user_id"] = str(user_id)
    if project_id is not None:
        params["project_id"] = str(project_id)
    if from_date is not None:
        params["from"] = from_date.isoformat() if isinstance(from_date, date) else from_date
    if to_date is not None:
        params["to"] = to_date.isoformat() if isinstance(to_date, date) else to_date
    return [
        HarvestTimeEntry.model_validate(item)
        for item in self._paginate("/time_entries", "time_entries", params=params)
    ]

create_time_entry

create_time_entry(
    *,
    project_id: int,
    task_id: int,
    spent_date: str | date,
    hours: float,
    user_id: int | None = None,
    notes: str | None = None,
) -> HarvestTimeEntry

Create a new time entry.

Parameters:

Name Type Description Default
project_id int

Project ID to log time against.

required
task_id int

Task ID to log time against.

required
spent_date str | date

Date the time was spent (ISO string or date object).

required
hours float

Number of hours to log.

required
user_id int | None

User ID to log time for (admin-only; regular users cannot set this).

None
notes str | None

Optional notes for the time entry.

None

Returns:

Type Description
HarvestTimeEntry

The created HarvestTimeEntry object.

Raises:

Type Description
ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_harvest/client.py
def create_time_entry(
    self,
    *,
    project_id: int,
    task_id: int,
    spent_date: str | date,
    hours: float,
    user_id: int | None = None,
    notes: str | None = None,
) -> HarvestTimeEntry:
    """Create a new time entry.

    Args:
        project_id: Project ID to log time against.
        task_id: Task ID to log time against.
        spent_date: Date the time was spent (ISO string or date object).
        hours: Number of hours to log.
        user_id: User ID to log time for (admin-only; regular users
            cannot set this).
        notes: Optional notes for the time entry.

    Returns:
        The created HarvestTimeEntry object.

    Raises:
        ForecastHTTPError: On HTTP errors.
    """
    spent_str = spent_date.isoformat() if isinstance(spent_date, date) else spent_date
    body: dict[str, Any] = {
        "project_id": project_id,
        "task_id": task_id,
        "spent_date": spent_str,
        "hours": hours,
    }
    if user_id is not None:
        body["user_id"] = user_id
    if notes is not None:
        body["notes"] = notes
    data = self._post("/time_entries", body)
    return HarvestTimeEntry.model_validate(data)

list_user_assignments

list_user_assignments(
    project_id: int,
) -> list[HarvestUserAssignment]

List user assignments for a project.

Parameters:

Name Type Description Default
project_id int

Project ID to list user assignments for.

required

Returns:

Type Description
list[HarvestUserAssignment]

List of HarvestUserAssignment objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_user_assignments(self, project_id: int) -> list[HarvestUserAssignment]:
    """List user assignments for a project.

    Args:
        project_id: Project ID to list user assignments for.

    Returns:
        List of HarvestUserAssignment objects.
    """
    return [
        HarvestUserAssignment.model_validate(item)
        for item in self._paginate(
            f"/projects/{project_id}/user_assignments", "user_assignments"
        )
    ]

whoami

whoami() -> HarvestCurrentUser

Retrieve the current authenticated user.

Returns:

Type Description
HarvestCurrentUser

HarvestCurrentUser object.

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

    Returns:
        HarvestCurrentUser object.
    """
    data = self._get("/users/me")
    return HarvestCurrentUser.model_validate(data)

HarvestClient

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

Sync client for the Harvest API v2.

Example

with SyncHarvestClient( access_token="token", account_id="123", user_agent="my-app (you@example.com)", ) as client: projects = client.list_projects()

Initialize the sync Harvest client.

Parameters:

Name Type Description Default
access_token str

Harvest personal access token.

required
account_id str

Harvest account ID.

required
user_agent str

User-Agent header value sent with every request.

required
base_url str

Harvest API base URL.

'https://api.harvestapp.com/v2'
timeout float

Request timeout in seconds.

30.0
retry RetryPolicy | None

Retry policy for transient failures.

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

    Args:
        access_token: Harvest personal access token.
        account_id: Harvest account ID.
        user_agent: User-Agent header value sent with every request.
        base_url: Harvest API base URL.
        timeout: Request timeout in seconds.
        retry: Retry policy for transient failures.
    """
    self._retry = retry or RetryPolicy()
    self._client = httpx.Client(
        base_url=base_url,
        timeout=httpx.Timeout(timeout, connect=10.0),
        headers={
            "Authorization": f"Bearer {access_token}",
            "Harvest-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 context manager.

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

__exit__

__exit__(*_: object) -> None

Exit the context manager, closing the HTTP client.

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

close

close() -> None

Close the underlying HTTP client.

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

list_projects

list_projects(
    *,
    is_active: bool | None = None,
    client_id: int | None = None,
) -> list[HarvestProject]

List all projects in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None
client_id int | None

Filter by client ID.

None

Returns:

Type Description
list[HarvestProject]

List of HarvestProject objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_projects(
    self, *, is_active: bool | None = None, client_id: int | None = None
) -> list[HarvestProject]:
    """List all projects in the Harvest account.

    Args:
        is_active: Filter by active status.
        client_id: Filter by client ID.

    Returns:
        List of HarvestProject objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    if client_id is not None:
        params["client_id"] = str(client_id)
    return [
        HarvestProject.model_validate(item)
        for item in self._paginate("/projects", "projects", params=params)
    ]

list_users

list_users(
    *, is_active: bool | None = None
) -> list[HarvestUser]

List all users in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None

Returns:

Type Description
list[HarvestUser]

List of HarvestUser objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_users(self, *, is_active: bool | None = None) -> list[HarvestUser]:
    """List all users in the Harvest account.

    Args:
        is_active: Filter by active status.

    Returns:
        List of HarvestUser objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    return [
        HarvestUser.model_validate(item)
        for item in self._paginate("/users", "users", params=params)
    ]

list_clients

list_clients(
    *, is_active: bool | None = None
) -> list[HarvestClient]

List all clients in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None

Returns:

Type Description
list[HarvestClient]

List of HarvestClient objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_clients(self, *, is_active: bool | None = None) -> list[HarvestClient]:
    """List all clients in the Harvest account.

    Args:
        is_active: Filter by active status.

    Returns:
        List of HarvestClient objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    return [
        HarvestClient.model_validate(item)
        for item in self._paginate("/clients", "clients", params=params)
    ]

list_tasks

list_tasks(
    *, is_active: bool | None = None
) -> list[HarvestTask]

List all tasks in the Harvest account.

Parameters:

Name Type Description Default
is_active bool | None

Filter by active status.

None

Returns:

Type Description
list[HarvestTask]

List of HarvestTask objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_tasks(self, *, is_active: bool | None = None) -> list[HarvestTask]:
    """List all tasks in the Harvest account.

    Args:
        is_active: Filter by active status.

    Returns:
        List of HarvestTask objects.
    """
    params: dict[str, str] = {}
    if is_active is not None:
        params["is_active"] = "true" if is_active else "false"
    return [
        HarvestTask.model_validate(item)
        for item in self._paginate("/tasks", "tasks", params=params)
    ]

list_time_entries

list_time_entries(
    *,
    user_id: int | None = None,
    project_id: int | None = None,
    from_date: str | date | None = None,
    to_date: str | date | None = None,
) -> list[HarvestTimeEntry]

List time entries, optionally filtered.

Parameters:

Name Type Description Default
user_id int | None

Filter by user ID.

None
project_id int | None

Filter by project ID.

None
from_date str | date | None

Start date (ISO string or date object).

None
to_date str | date | None

End date (ISO string or date object).

None

Returns:

Type Description
list[HarvestTimeEntry]

List of HarvestTimeEntry objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_time_entries(
    self,
    *,
    user_id: int | None = None,
    project_id: int | None = None,
    from_date: str | date | None = None,
    to_date: str | date | None = None,
) -> list[HarvestTimeEntry]:
    """List time entries, optionally filtered.

    Args:
        user_id: Filter by user ID.
        project_id: Filter by project ID.
        from_date: Start date (ISO string or date object).
        to_date: End date (ISO string or date object).

    Returns:
        List of HarvestTimeEntry objects.
    """
    params: dict[str, str] = {}
    if user_id is not None:
        params["user_id"] = str(user_id)
    if project_id is not None:
        params["project_id"] = str(project_id)
    if from_date is not None:
        params["from"] = from_date.isoformat() if isinstance(from_date, date) else from_date
    if to_date is not None:
        params["to"] = to_date.isoformat() if isinstance(to_date, date) else to_date
    return [
        HarvestTimeEntry.model_validate(item)
        for item in self._paginate("/time_entries", "time_entries", params=params)
    ]

create_time_entry

create_time_entry(
    *,
    project_id: int,
    task_id: int,
    spent_date: str | date,
    hours: float,
    user_id: int | None = None,
    notes: str | None = None,
) -> HarvestTimeEntry

Create a new time entry.

Parameters:

Name Type Description Default
project_id int

Project ID to log time against.

required
task_id int

Task ID to log time against.

required
spent_date str | date

Date the time was spent (ISO string or date object).

required
hours float

Number of hours to log.

required
user_id int | None

User ID to log time for (admin-only; regular users cannot set this).

None
notes str | None

Optional notes for the time entry.

None

Returns:

Type Description
HarvestTimeEntry

The created HarvestTimeEntry object.

Raises:

Type Description
ForecastHTTPError

On HTTP errors.

Source code in src/harvest_forecast/_harvest/client.py
def create_time_entry(
    self,
    *,
    project_id: int,
    task_id: int,
    spent_date: str | date,
    hours: float,
    user_id: int | None = None,
    notes: str | None = None,
) -> HarvestTimeEntry:
    """Create a new time entry.

    Args:
        project_id: Project ID to log time against.
        task_id: Task ID to log time against.
        spent_date: Date the time was spent (ISO string or date object).
        hours: Number of hours to log.
        user_id: User ID to log time for (admin-only; regular users
            cannot set this).
        notes: Optional notes for the time entry.

    Returns:
        The created HarvestTimeEntry object.

    Raises:
        ForecastHTTPError: On HTTP errors.
    """
    spent_str = spent_date.isoformat() if isinstance(spent_date, date) else spent_date
    body: dict[str, Any] = {
        "project_id": project_id,
        "task_id": task_id,
        "spent_date": spent_str,
        "hours": hours,
    }
    if user_id is not None:
        body["user_id"] = user_id
    if notes is not None:
        body["notes"] = notes
    data = self._post("/time_entries", body)
    return HarvestTimeEntry.model_validate(data)

list_user_assignments

list_user_assignments(
    project_id: int,
) -> list[HarvestUserAssignment]

List user assignments for a project.

Parameters:

Name Type Description Default
project_id int

Project ID to list user assignments for.

required

Returns:

Type Description
list[HarvestUserAssignment]

List of HarvestUserAssignment objects.

Source code in src/harvest_forecast/_harvest/client.py
def list_user_assignments(self, project_id: int) -> list[HarvestUserAssignment]:
    """List user assignments for a project.

    Args:
        project_id: Project ID to list user assignments for.

    Returns:
        List of HarvestUserAssignment objects.
    """
    return [
        HarvestUserAssignment.model_validate(item)
        for item in self._paginate(
            f"/projects/{project_id}/user_assignments", "user_assignments"
        )
    ]

whoami

whoami() -> HarvestCurrentUser

Retrieve the current authenticated user.

Returns:

Type Description
HarvestCurrentUser

HarvestCurrentUser object.

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

    Returns:
        HarvestCurrentUser object.
    """
    data = self._get("/users/me")
    return HarvestCurrentUser.model_validate(data)

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]

ForecastAuthError

ForecastAuthError(
    status_code: int, response_body: str, url: str
)

Bases: ForecastHTTPError

401 or 403 — authentication or authorization failure.

Source code in src/harvest_forecast/exceptions.py
def __init__(self, status_code: int, response_body: str, url: str) -> None:
    self.status_code = status_code
    self.response_body = response_body
    self.url = url
    super().__init__(self._format_message())

ForecastError

Bases: Exception

Base for all exceptions raised by harvest_forecast.

ForecastHTTPError

ForecastHTTPError(
    status_code: int, response_body: str, url: str
)

Bases: ForecastError

HTTP error from the Forecast API.

Carries status_code, response_body, and url. Subclasses map specific status-code ranges; this class serves as the catch-all for unrecognised codes and as the base for all HTTP exceptions.

Source code in src/harvest_forecast/exceptions.py
def __init__(self, status_code: int, response_body: str, url: str) -> None:
    self.status_code = status_code
    self.response_body = response_body
    self.url = url
    super().__init__(self._format_message())

status_code instance-attribute

status_code: int = status_code

response_body instance-attribute

response_body: str = response_body

url instance-attribute

url: str = url

from_response classmethod

from_response(response: Response) -> ForecastHTTPError
Source code in src/harvest_forecast/exceptions.py
@classmethod
def from_response(cls, response: httpx.Response) -> ForecastHTTPError:
    status = response.status_code
    body = response.text
    url = str(response.url)

    if status in (HTTP_UNAUTHORIZED, HTTP_FORBIDDEN):
        return ForecastAuthError(status, body, url)
    if status == HTTP_NOT_FOUND:
        return ForecastNotFoundError(status, body, url)
    if status == HTTP_TOO_MANY:
        return ForecastRateLimitError(
            status, body, url, retry_after=_parse_retry_after(response)
        )
    if HTTP_SERVER_MIN <= status < HTTP_SERVER_MAX:
        return ForecastServerError(status, body, url)
    if status in (HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE):
        return ForecastValidationError(status, body, url)
    return ForecastHTTPError(status, body, url)

ForecastNotFoundError

ForecastNotFoundError(
    status_code: int, response_body: str, url: str
)

Bases: ForecastHTTPError

404 — resource not found.

Source code in src/harvest_forecast/exceptions.py
def __init__(self, status_code: int, response_body: str, url: str) -> None:
    self.status_code = status_code
    self.response_body = response_body
    self.url = url
    super().__init__(self._format_message())

ForecastRateLimitError

ForecastRateLimitError(
    status_code: int,
    response_body: str,
    url: str,
    *,
    retry_after: float | None = None,
)

Bases: ForecastHTTPError

429 — rate limited. Carries retry_after from the Retry-After header.

Source code in src/harvest_forecast/exceptions.py
def __init__(
    self,
    status_code: int,
    response_body: str,
    url: str,
    *,
    retry_after: float | None = None,
) -> None:
    self.retry_after = retry_after
    super().__init__(status_code, response_body, url)

retry_after instance-attribute

retry_after: float | None = retry_after

ForecastServerError

ForecastServerError(
    status_code: int, response_body: str, url: str
)

Bases: ForecastHTTPError

5xx — server-side error.

Source code in src/harvest_forecast/exceptions.py
def __init__(self, status_code: int, response_body: str, url: str) -> None:
    self.status_code = status_code
    self.response_body = response_body
    self.url = url
    super().__init__(self._format_message())

ForecastValidationError

ForecastValidationError(
    status_code: int, response_body: str, url: str
)

Bases: ForecastHTTPError

400 or 422 — API returned a validation error body.

Source code in src/harvest_forecast/exceptions.py
def __init__(self, status_code: int, response_body: str, url: str) -> None:
    self.status_code = status_code
    self.response_body = response_body
    self.url = url
    super().__init__(self._format_message())

RetryPolicy dataclass

RetryPolicy(
    max_attempts: int = 6,
    initial_seconds: float = 1.0,
    max_seconds: float = 60.0,
    jitter_seconds: float = 2.0,
)

How the Forecast client handles transient failures.

max_attempts class-attribute instance-attribute

max_attempts: int = 6

initial_seconds class-attribute instance-attribute

initial_seconds: float = 1.0

max_seconds class-attribute instance-attribute

max_seconds: float = 60.0

jitter_seconds class-attribute instance-attribute

jitter_seconds: float = 2.0

no_wait classmethod

no_wait() -> RetryPolicy

Test-friendly: zero backoff, single attempt.

Source code in src/harvest_forecast/retry.py
@classmethod
def no_wait(cls) -> RetryPolicy:
    """Test-friendly: zero backoff, single attempt."""
    return cls(max_attempts=1, initial_seconds=0.0, max_seconds=0.0, jitter_seconds=0.0)

fast_test classmethod

fast_test(max_attempts: int = 3) -> RetryPolicy

Test-friendly: zero backoff, configurable attempt count.

Source code in src/harvest_forecast/retry.py
@classmethod
def fast_test(cls, max_attempts: int = 3) -> RetryPolicy:
    """Test-friendly: zero backoff, configurable attempt count."""
    return cls(
        max_attempts=max_attempts,
        initial_seconds=0.0,
        max_seconds=0.0,
        jitter_seconds=0.0,
    )

wait

wait(state: RetryCallState) -> float
Source code in src/harvest_forecast/retry.py
def wait(self, state: RetryCallState) -> float:
    if self.initial_seconds == 0.0 and self.max_seconds == 0.0:
        base = 0.0
    else:
        base = wait_exponential_jitter(
            initial=self.initial_seconds,
            max=self.max_seconds,
            jitter=self.jitter_seconds,
        )(state)
    exc = state.outcome.exception() if state.outcome else None
    if isinstance(exc, ForecastRateLimitError) and exc.retry_after is not None:
        return max(base, exc.retry_after) if self.max_seconds > 0 else 0.0
    return base

Account

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

weekly_capacity class-attribute instance-attribute

weekly_capacity: int | None = None

color_labels class-attribute instance-attribute

color_labels: list[ColorLabel] = Field(
    default_factory=list[ColorLabel]
)

harvest_subdomain class-attribute instance-attribute

harvest_subdomain: str | None = None
harvest_link: str | None = None

saml_sign_in_required class-attribute instance-attribute

saml_sign_in_required: bool = False

harvest_name class-attribute instance-attribute

harvest_name: str | None = None

weekends_enabled class-attribute instance-attribute

weekends_enabled: bool = False

created_at class-attribute instance-attribute

created_at: datetime | None = None

creator_first_name class-attribute instance-attribute

creator_first_name: str | None = None

creator_last_name class-attribute instance-attribute

creator_last_name: str | None = None

billing_status class-attribute instance-attribute

billing_status: str | None = None

gdpr class-attribute instance-attribute

gdpr: bool = False

Assignment

Bases: ForecastModel

id instance-attribute

id: int

start_date instance-attribute

start_date: date

end_date instance-attribute

end_date: date

allocation class-attribute instance-attribute

allocation: int | None = None

notes class-attribute instance-attribute

notes: str | None = None

updated_at instance-attribute

updated_at: datetime

updated_by_id class-attribute instance-attribute

updated_by_id: int | None = None

project_id class-attribute instance-attribute

project_id: int | None = None

person_id class-attribute instance-attribute

person_id: int | None = None

placeholder_id class-attribute instance-attribute

placeholder_id: int | None = None

repeated_assignment_set_id class-attribute instance-attribute

repeated_assignment_set_id: int | None = None

harvest_project_task_id class-attribute instance-attribute

harvest_project_task_id: int | None = None

active_on_days_off class-attribute instance-attribute

active_on_days_off: bool = False

AssignmentFilter dataclass

AssignmentFilter(
    project_id: int | None = None,
    person_id: int | None = None,
    start_date: date | None = None,
    end_date: date | None = None,
    repeated_assignment_set_id: int | None = None,
    state: str | None = None,
)

project_id class-attribute instance-attribute

project_id: int | None = None

person_id class-attribute instance-attribute

person_id: int | None = None

start_date class-attribute instance-attribute

start_date: date | None = None

end_date class-attribute instance-attribute

end_date: date | None = None

repeated_assignment_set_id class-attribute instance-attribute

repeated_assignment_set_id: int | None = None

state class-attribute instance-attribute

state: str | None = None

to_params

to_params() -> dict[str, str]
Source code in src/harvest_forecast/schemas.py
def to_params(self) -> dict[str, str]:
    params: dict[str, str] = {}
    for f in fields(self):
        value = getattr(self, f.name)
        if value is None:
            continue
        if isinstance(value, date):
            params[f.name] = value.isoformat()
        else:
            params[f.name] = str(value)
    return params

AssignmentRequest dataclass

AssignmentRequest(
    start_date: date,
    end_date: date,
    project_id: int,
    person_id: int,
    allocation: int | None = None,
    notes: str | None = None,
    placeholder_id: int | None = None,
    repeated_assignment_set_id: int | None = None,
    active_on_days_off: bool = False,
    harvest_project_task_id: int | None = None,
)

start_date instance-attribute

start_date: date

end_date instance-attribute

end_date: date

project_id instance-attribute

project_id: int

person_id instance-attribute

person_id: int

allocation class-attribute instance-attribute

allocation: int | None = None

notes class-attribute instance-attribute

notes: str | None = None

placeholder_id class-attribute instance-attribute

placeholder_id: int | None = None

repeated_assignment_set_id class-attribute instance-attribute

repeated_assignment_set_id: int | None = None

active_on_days_off class-attribute instance-attribute

active_on_days_off: bool = False

harvest_project_task_id class-attribute instance-attribute

harvest_project_task_id: int | None = None

to_payload

to_payload() -> dict[str, Any]
Source code in src/harvest_forecast/schemas.py
def to_payload(self) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "start_date": self.start_date.isoformat(),
        "end_date": self.end_date.isoformat(),
        "project_id": self.project_id,
        "person_id": self.person_id,
        "active_on_days_off": self.active_on_days_off,
    }
    if self.allocation is not None:
        payload["allocation"] = self.allocation
    if self.notes is not None:
        payload["notes"] = self.notes
    if self.placeholder_id is not None:
        payload["placeholder_id"] = self.placeholder_id
    if self.repeated_assignment_set_id is not None:
        payload["repeated_assignment_set_id"] = self.repeated_assignment_set_id
    if self.harvest_project_task_id is not None:
        payload["harvest_project_task_id"] = self.harvest_project_task_id
    return {"assignment": payload}

Client

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

harvest_id class-attribute instance-attribute

harvest_id: int | None = None

archived class-attribute instance-attribute

archived: bool = False

updated_at instance-attribute

updated_at: datetime

updated_by_id class-attribute instance-attribute

updated_by_id: int | None = None

ColorLabel

Bases: ForecastModel

name instance-attribute

name: str

label instance-attribute

label: str

CurrentUser

Bases: ForecastModel

id instance-attribute

id: int

account_ids class-attribute instance-attribute

account_ids: list[int] = Field(default_factory=list[int])

identity_user_id class-attribute instance-attribute

identity_user_id: int | None = None

ForecastModel

Bases: BaseModel

model_config class-attribute instance-attribute

model_config = ConfigDict(
    extra="allow",
    populate_by_name=True,
    str_strip_whitespace=True,
    frozen=True,
)

FutureScheduledHoursItem

Bases: ForecastModel

project_id class-attribute instance-attribute

project_id: int | None = None

person_id class-attribute instance-attribute

person_id: int | None = None

placeholder_id class-attribute instance-attribute

placeholder_id: int | None = None

allocation class-attribute instance-attribute

allocation: float | None = None

HarvestClientRef

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

currency class-attribute instance-attribute

currency: str | None = None

HarvestCurrentUser

Bases: ForecastModel

id instance-attribute

id: int

first_name instance-attribute

first_name: str

last_name instance-attribute

last_name: str

email instance-attribute

email: str

timezone class-attribute instance-attribute

timezone: str | None = None

is_admin class-attribute instance-attribute

is_admin: bool = False

is_project_manager class-attribute instance-attribute

is_project_manager: bool = False

can_see_project_billable_rates class-attribute instance-attribute

can_see_project_billable_rates: bool = False

can_approve_timesheets class-attribute instance-attribute

can_approve_timesheets: bool = False

roles class-attribute instance-attribute

roles: list[str] = Field(default_factory=list[str])

HarvestProject

Bases: ForecastModel

id instance-attribute

id: int

client instance-attribute

client: HarvestClientRef

name instance-attribute

name: str

code class-attribute instance-attribute

code: str | None = None

is_active instance-attribute

is_active: bool

is_billable instance-attribute

is_billable: bool

is_fixed_fee instance-attribute

is_fixed_fee: bool

bill_by instance-attribute

bill_by: str

budget class-attribute instance-attribute

budget: Decimal | None = None

budget_by instance-attribute

budget_by: str

budget_is_monthly instance-attribute

budget_is_monthly: bool

created_at instance-attribute

created_at: datetime

updated_at instance-attribute

updated_at: datetime

HarvestProjectRef

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

code class-attribute instance-attribute

code: str | None = None

HarvestTask

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

billable_by_default instance-attribute

billable_by_default: bool

is_default instance-attribute

is_default: bool

is_active instance-attribute

is_active: bool

default_hourly_rate class-attribute instance-attribute

default_hourly_rate: Decimal | None = None

created_at instance-attribute

created_at: datetime

updated_at instance-attribute

updated_at: datetime

HarvestTaskRef

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

HarvestTimeEntry

Bases: ForecastModel

id instance-attribute

id: int

spent_date instance-attribute

spent_date: date

user instance-attribute

user: HarvestUserRef

client instance-attribute

client: HarvestClientRef

project instance-attribute

project: HarvestProjectRef

task instance-attribute

task: HarvestTaskRef

hours instance-attribute

hours: Decimal

notes class-attribute instance-attribute

notes: str | None = None

is_locked instance-attribute

is_locked: bool

is_closed instance-attribute

is_closed: bool

is_billed instance-attribute

is_billed: bool

billable instance-attribute

billable: bool

created_at instance-attribute

created_at: datetime

updated_at instance-attribute

updated_at: datetime

billable_rate class-attribute instance-attribute

billable_rate: Decimal | None = None

cost_rate class-attribute instance-attribute

cost_rate: Decimal | None = None

HarvestUser

Bases: ForecastModel

id instance-attribute

id: int

first_name instance-attribute

first_name: str

last_name instance-attribute

last_name: str

email instance-attribute

email: str

is_active instance-attribute

is_active: bool

is_contractor instance-attribute

is_contractor: bool

weekly_capacity class-attribute instance-attribute

weekly_capacity: int | None = None

default_hourly_rate class-attribute instance-attribute

default_hourly_rate: Decimal | None = None

cost_rate class-attribute instance-attribute

cost_rate: Decimal | None = None

roles class-attribute instance-attribute

roles: list[str] = Field(default_factory=list[str])

created_at instance-attribute

created_at: datetime

updated_at instance-attribute

updated_at: datetime

HarvestUserAssignment

Bases: ForecastModel

id instance-attribute

id: int

project instance-attribute

project: HarvestProjectRef

user instance-attribute

user: HarvestUserRef

is_active instance-attribute

is_active: bool

is_project_manager instance-attribute

is_project_manager: bool

use_default_rates instance-attribute

use_default_rates: bool

hourly_rate class-attribute instance-attribute

hourly_rate: Decimal | None = None

budget class-attribute instance-attribute

budget: Decimal | None = None

created_at instance-attribute

created_at: datetime

updated_at instance-attribute

updated_at: datetime

HarvestUserRef

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

Milestone

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

date instance-attribute

date: date

updated_at instance-attribute

updated_at: datetime

updated_by_id class-attribute instance-attribute

updated_by_id: int | None = None

project_id class-attribute instance-attribute

project_id: int | None = None

Person

Bases: ForecastModel

id instance-attribute

id: int

first_name instance-attribute

first_name: str

last_name instance-attribute

last_name: str

email class-attribute instance-attribute

email: str | None = None

login class-attribute instance-attribute

login: str | None = None

admin class-attribute instance-attribute

admin: bool = False

archived class-attribute instance-attribute

archived: bool = False

subscribed class-attribute instance-attribute

subscribed: bool = False

avatar_url class-attribute instance-attribute

avatar_url: str | None = None

teams class-attribute instance-attribute

teams: list[str] = Field(default_factory=list[str])

updated_at instance-attribute

updated_at: datetime

updated_by_id class-attribute instance-attribute

updated_by_id: int | None = None

harvest_user_id class-attribute instance-attribute

harvest_user_id: int | None = None

weekly_capacity class-attribute instance-attribute

weekly_capacity: int | None = None

working_days class-attribute instance-attribute

working_days: WorkingDays | None = None

color_blind class-attribute instance-attribute

color_blind: bool = False

roles class-attribute instance-attribute

roles: list[str] = Field(default_factory=list[str])

invitation_code_id class-attribute instance-attribute

invitation_code_id: int | None = None

personal_feed_token_id class-attribute instance-attribute

personal_feed_token_id: int | None = None

PersonHeatmapItem

Bases: ForecastModel

start_date class-attribute instance-attribute

start_date: str | None = None

end_date class-attribute instance-attribute

end_date: str | None = None

daily_allocation class-attribute instance-attribute

daily_allocation: int | None = None

daily_time_off class-attribute instance-attribute

daily_time_off: int | None = None

Placeholder

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

archived class-attribute instance-attribute

archived: bool = False

teams class-attribute instance-attribute

teams: list[str] = Field(default_factory=list[str])

roles class-attribute instance-attribute

roles: list[str] = Field(default_factory=list[str])

updated_at instance-attribute

updated_at: datetime

updated_by_id class-attribute instance-attribute

updated_by_id: int | None = None

PlaceholderHeatmapItem

Bases: ForecastModel

start_date class-attribute instance-attribute

start_date: str | None = None

end_date class-attribute instance-attribute

end_date: str | None = None

daily_allocation class-attribute instance-attribute

daily_allocation: int | None = None

daily_time_off class-attribute instance-attribute

daily_time_off: int | None = None

Project

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

color class-attribute instance-attribute

color: str | None = None

code class-attribute instance-attribute

code: str | None = None

notes class-attribute instance-attribute

notes: str | None = None

start_date class-attribute instance-attribute

start_date: date | None = None

end_date class-attribute instance-attribute

end_date: date | None = None

harvest_id class-attribute instance-attribute

harvest_id: int | None = None

archived class-attribute instance-attribute

archived: bool = False

budget_by class-attribute instance-attribute

budget_by: str | None = None

budget_is_monthly class-attribute instance-attribute

budget_is_monthly: bool = False

updated_at instance-attribute

updated_at: datetime

updated_by_id class-attribute instance-attribute

updated_by_id: int | None = None

client_id class-attribute instance-attribute

client_id: int | None = None

tags class-attribute instance-attribute

tags: list[str] = Field(default_factory=list[str])

ProjectHeatmapItem

Bases: ForecastModel

start_date class-attribute instance-attribute

start_date: str | None = None

end_date class-attribute instance-attribute

end_date: str | None = None

RemainingBudgetedHoursItem

Bases: ForecastModel

project_id instance-attribute

project_id: int

budget_by class-attribute instance-attribute

budget_by: str | None = None

budget_is_monthly class-attribute instance-attribute

budget_is_monthly: bool = False

hours class-attribute instance-attribute

hours: float | None = None

response_code class-attribute instance-attribute

response_code: int | None = None

RepeatedAssignmentSet

Bases: ForecastModel

id instance-attribute

id: int

first_start_date class-attribute instance-attribute

first_start_date: date | None = None

last_end_date class-attribute instance-attribute

last_end_date: date | None = None

assignment_ids class-attribute instance-attribute

assignment_ids: list[int] = Field(default_factory=list[int])

Role

Bases: ForecastModel

id instance-attribute

id: int

name instance-attribute

name: str

placeholder_ids class-attribute instance-attribute

placeholder_ids: list[int] = Field(
    default_factory=list[int]
)

person_ids class-attribute instance-attribute

person_ids: list[int] = Field(default_factory=list[int])

harvest_role_id class-attribute instance-attribute

harvest_role_id: int | None = None

Subscription

Bases: ForecastModel

id instance-attribute

id: int

interval_unit_amounts class-attribute instance-attribute

interval_unit_amounts: (
    SubscriptionIntervalUnitAmounts | None
) = None

next_billing_date class-attribute instance-attribute

next_billing_date: str | None = None

days_until_next_billing_date class-attribute instance-attribute

days_until_next_billing_date: int | None = None

amount class-attribute instance-attribute

amount: int | None = None

default_deactivation_at class-attribute instance-attribute

default_deactivation_at: datetime | None = None

receipt_recipient class-attribute instance-attribute

receipt_recipient: str | None = None

status class-attribute instance-attribute

status: str | None = None

purchased_people class-attribute instance-attribute

purchased_people: int | None = None

interval class-attribute instance-attribute

interval: str | None = None

discounts class-attribute instance-attribute

discounts: SubscriptionDiscounts | None = None

placeholder_limit class-attribute instance-attribute

placeholder_limit: int | None = None

invoiced class-attribute instance-attribute

invoiced: bool = False

days_until_due class-attribute instance-attribute

days_until_due: int | None = None

balance class-attribute instance-attribute

balance: int | None = None

past_due_balance class-attribute instance-attribute

past_due_balance: int | None = None

sales_tax_exempt class-attribute instance-attribute

sales_tax_exempt: bool = False

sales_tax_percentage class-attribute instance-attribute

sales_tax_percentage: float | None = None

converted_at class-attribute instance-attribute

converted_at: datetime | None = None

card class-attribute instance-attribute

card: SubscriptionCard | None = None

address class-attribute instance-attribute

address: SubscriptionAddress | None = None

SubscriptionAddress

Bases: ForecastModel

line_1 class-attribute instance-attribute

line_1: str | None = None

line_2 class-attribute instance-attribute

line_2: str | None = None

city class-attribute instance-attribute

city: str | None = None

state class-attribute instance-attribute

state: str | None = None

postal_code class-attribute instance-attribute

postal_code: str | None = None

country class-attribute instance-attribute

country: str | None = None

SubscriptionCard

Bases: ForecastModel

brand class-attribute instance-attribute

brand: str | None = None

last_four class-attribute instance-attribute

last_four: str | None = None

expiry_month class-attribute instance-attribute

expiry_month: int | None = None

expiry_year class-attribute instance-attribute

expiry_year: int | None = None

SubscriptionDiscounts

Bases: ForecastModel

monthly_percentage class-attribute instance-attribute

monthly_percentage: float = 0.0

yearly_percentage class-attribute instance-attribute

yearly_percentage: float = 0.0

SubscriptionIntervalUnitAmounts

Bases: ForecastModel

monthly class-attribute instance-attribute

monthly: int = 0

yearly class-attribute instance-attribute

yearly: int = 0

UserConnection

Bases: ForecastModel

id instance-attribute

id: int

person_id class-attribute instance-attribute

person_id: int | None = None

last_active_at class-attribute instance-attribute

last_active_at: datetime | None = None

WorkingDays

Bases: ForecastModel

monday class-attribute instance-attribute

monday: bool = False

tuesday class-attribute instance-attribute

tuesday: bool = False

wednesday class-attribute instance-attribute

wednesday: bool = False

thursday class-attribute instance-attribute

thursday: bool = False

friday class-attribute instance-attribute

friday: bool = False

saturday class-attribute instance-attribute

saturday: bool = False

sunday class-attribute instance-attribute

sunday: bool = False