Skip to content

API Reference

This page contains the automatically generated API reference documentation for the folioman-client library, extracted directly from the Python source docstrings.


Package Root

folioman_client

Folioman API Client Package.

Typed asynchronous Python SDK for the Folioman REST API.

ConfiguredDate module-attribute

ConfiguredDate = Annotated[date, PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used='unless-none')]

Date type serialized to ISO-8601 string (YYYY-MM-DD) unless None.

ConfiguredDatetime module-attribute

ConfiguredDatetime = Annotated[datetime, PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used='unless-none')]

Datetime type serialized to ISO-8601 string unless None.

ConfiguredDecimal module-attribute

ConfiguredDecimal = Annotated[Decimal, PlainSerializer(lambda x: float(x), return_type=float, when_used='unless-none')]

Decimal type serialized to float unless None.

JWTAuthManager

Manages JWT authentication state, token storage, and refresh lifecycle.

Keeps tokens private so they are never leaked outside the client.

Source code in src/folioman_client/auth.py
class JWTAuthManager:
    """Manages JWT authentication state, token storage, and refresh lifecycle.

    Keeps tokens private so they are never leaked outside the client.
    """

    def __init__(
        self,
        base_url: str,
        username: str,
        password: str,
    ) -> None:
        """Initialize JWTAuthManager with API credentials.

        Args:
            base_url: Base URL of the Folioman server.
            username: Username for authentication.
            password: Password for authentication.
        """
        self._base_url = base_url.rstrip("/")
        self._username = username
        self._password = password
        self._access_token: str | None = None
        self._refresh_token: str | None = None
        self._lock = asyncio.Lock()

    @property
    def has_tokens(self) -> bool:
        """True if the manager currently holds an access or refresh token."""
        return self._access_token is not None or self._refresh_token is not None

    def clear(self) -> None:
        """Clear cached access and refresh tokens."""
        self._access_token = None
        self._refresh_token = None

    async def get_valid_token(self, client: httpx.AsyncClient) -> str:
        """Return a valid access token, proactively refreshing or authenticating if needed.

        Args:
            client: The httpx AsyncClient transport instance to execute requests.

        Returns:
            A valid JWT access token string.

        Raises:
            FoliomanAuthError: If authentication or refresh fails.
        """
        # Fast path outside the lock if the current access token is fresh
        if self._access_token and not _is_expired(self._access_token):
            return self._access_token

        async with self._lock:
            # Re-check under lock in case another coroutine refreshed it
            if self._access_token and not _is_expired(self._access_token):
                return self._access_token

            # Try refresh if we have a refresh token
            if self._refresh_token:
                try:
                    return await self._refresh_access_token(client)
                except Exception:
                    # If refresh fails, fall back to initial authentication
                    pass

            # Otherwise, authenticate from credentials
            return await self._authenticate(client)

    async def force_refresh(self, client: httpx.AsyncClient) -> str:
        """Force a refresh or re-authentication after receiving a 401.

        Args:
            client: The httpx AsyncClient transport instance to execute requests.

        Returns:
            A new valid JWT access token string.

        Raises:
            FoliomanAuthError: If renewal or re-authentication fails.
        """
        async with self._lock:
            if self._refresh_token:
                try:
                    return await self._refresh_access_token(client)
                except Exception:
                    pass

            return await self._authenticate(client)

    async def _authenticate(self, client: httpx.AsyncClient) -> str:
        """Authenticate with username and password (/api/auth/token/pair).

        Args:
            client: The httpx AsyncClient transport instance.

        Returns:
            A newly acquired access token string.

        Raises:
            FoliomanAuthError: If authentication fails or response is invalid.
        """
        url = f"{self._base_url}/api/auth/token/pair"
        payload = {"username": self._username, "password": self._password}

        try:
            response = await client.post(url, json=payload)
        except Exception as exc:
            raise FoliomanAuthError(
                f"Network error during authentication: {exc}"
            ) from exc

        if response.status_code == 401:
            raise FoliomanAuthError("Invalid username or password.")
        if not response.is_success:
            raise FoliomanAuthError(
                f"Authentication failed with status {response.status_code}: {response.text}"
            )

        data = response.json()
        access = data.get("access")
        refresh = data.get("refresh")
        if not access or not refresh:
            raise FoliomanAuthError(
                "Malformed authentication response: missing tokens."
            )

        self._access_token = access
        self._refresh_token = refresh
        return access

    async def _refresh_access_token(self, client: httpx.AsyncClient) -> str:
        """Mint a fresh access token from the refresh token (/api/auth/token/refresh).

        Args:
            client: The httpx AsyncClient transport instance.

        Returns:
            A refreshed access token string.

        Raises:
            FoliomanAuthError: If refresh token is expired, invalid, or missing.
        """
        if not self._refresh_token:
            raise FoliomanAuthError("No refresh token available.")

        url = f"{self._base_url}/api/auth/token/refresh"
        payload = {"refresh": self._refresh_token}

        try:
            response = await client.post(url, json=payload)
        except Exception as exc:
            raise FoliomanAuthError(
                f"Network error during token refresh: {exc}"
            ) from exc

        if response.status_code == 401:
            self.clear()
            raise FoliomanAuthError("Refresh token expired or invalid.")
        if not response.is_success:
            self.clear()
            raise FoliomanAuthError(
                f"Token refresh failed with status {response.status_code}: {response.text}"
            )

        data = response.json()
        access = data.get("access")
        if not access:
            self.clear()
            raise FoliomanAuthError("Malformed refresh response: missing access token.")

        self._access_token = access
        return access

has_tokens property

has_tokens: bool

True if the manager currently holds an access or refresh token.

__init__

__init__(base_url: str, username: str, password: str) -> None

Initialize JWTAuthManager with API credentials.

Parameters:

Name Type Description Default
base_url str

Base URL of the Folioman server.

required
username str

Username for authentication.

required
password str

Password for authentication.

required
Source code in src/folioman_client/auth.py
def __init__(
    self,
    base_url: str,
    username: str,
    password: str,
) -> None:
    """Initialize JWTAuthManager with API credentials.

    Args:
        base_url: Base URL of the Folioman server.
        username: Username for authentication.
        password: Password for authentication.
    """
    self._base_url = base_url.rstrip("/")
    self._username = username
    self._password = password
    self._access_token: str | None = None
    self._refresh_token: str | None = None
    self._lock = asyncio.Lock()

clear

clear() -> None

Clear cached access and refresh tokens.

Source code in src/folioman_client/auth.py
def clear(self) -> None:
    """Clear cached access and refresh tokens."""
    self._access_token = None
    self._refresh_token = None

get_valid_token async

get_valid_token(client: AsyncClient) -> str

Return a valid access token, proactively refreshing or authenticating if needed.

Parameters:

Name Type Description Default
client AsyncClient

The httpx AsyncClient transport instance to execute requests.

required

Returns:

Type Description
str

A valid JWT access token string.

Raises:

Type Description
FoliomanAuthError

If authentication or refresh fails.

Source code in src/folioman_client/auth.py
async def get_valid_token(self, client: httpx.AsyncClient) -> str:
    """Return a valid access token, proactively refreshing or authenticating if needed.

    Args:
        client: The httpx AsyncClient transport instance to execute requests.

    Returns:
        A valid JWT access token string.

    Raises:
        FoliomanAuthError: If authentication or refresh fails.
    """
    # Fast path outside the lock if the current access token is fresh
    if self._access_token and not _is_expired(self._access_token):
        return self._access_token

    async with self._lock:
        # Re-check under lock in case another coroutine refreshed it
        if self._access_token and not _is_expired(self._access_token):
            return self._access_token

        # Try refresh if we have a refresh token
        if self._refresh_token:
            try:
                return await self._refresh_access_token(client)
            except Exception:
                # If refresh fails, fall back to initial authentication
                pass

        # Otherwise, authenticate from credentials
        return await self._authenticate(client)

force_refresh async

force_refresh(client: AsyncClient) -> str

Force a refresh or re-authentication after receiving a 401.

Parameters:

Name Type Description Default
client AsyncClient

The httpx AsyncClient transport instance to execute requests.

required

Returns:

Type Description
str

A new valid JWT access token string.

Raises:

Type Description
FoliomanAuthError

If renewal or re-authentication fails.

Source code in src/folioman_client/auth.py
async def force_refresh(self, client: httpx.AsyncClient) -> str:
    """Force a refresh or re-authentication after receiving a 401.

    Args:
        client: The httpx AsyncClient transport instance to execute requests.

    Returns:
        A new valid JWT access token string.

    Raises:
        FoliomanAuthError: If renewal or re-authentication fails.
    """
    async with self._lock:
        if self._refresh_token:
            try:
                return await self._refresh_access_token(client)
            except Exception:
                pass

        return await self._authenticate(client)

CapitalGainsResource

Bases: _BaseResource

Endpoints for realised capital gains reporting.

Source code in src/folioman_client/client.py
class CapitalGainsResource(_BaseResource):
    """Endpoints for realised capital gains reporting."""

    async def list(
        self,
        investor_id: int,
        *,
        include_unreconciled: bool = False,
    ) -> list[CapitalGainsFyPoint]:
        """List realised STCG/LTCG across every financial year with disposals.

        Args:
            investor_id: The ID of the investor.
            include_unreconciled: Whether to include unreconciled transactions.

        Returns:
            List of CapitalGainsFyPoint models summarizing capital gains by financial year.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params = {"include_unreconciled": str(include_unreconciled).lower()}
        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/reports/capital-gains-by-fy",
            params=params,
        )
        return [CapitalGainsFyPoint.model_validate(item) for item in data]

    async def get(
        self,
        investor_id: int,
        *,
        fy: str,
        include_unreconciled: bool = False,
    ) -> CapitalGainsReport:
        """Get realised capital gains report for a specific financial year.

        Args:
            investor_id: The ID of the investor.
            fy: Financial year string, e.g. "2024-25".
            include_unreconciled: Whether to include unreconciled transactions.

        Returns:
            CapitalGainsReport model with detailed lot-level gains.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params = {
            "fy": fy,
            "include_unreconciled": str(include_unreconciled).lower(),
        }
        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/exports/capital-gains",
            params=params,
        )
        return CapitalGainsReport.model_validate(data)

list async

list(investor_id: int, *, include_unreconciled: bool = False) -> list[CapitalGainsFyPoint]

List realised STCG/LTCG across every financial year with disposals.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
include_unreconciled bool

Whether to include unreconciled transactions.

False

Returns:

Type Description
list[CapitalGainsFyPoint]

List of CapitalGainsFyPoint models summarizing capital gains by financial year.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    investor_id: int,
    *,
    include_unreconciled: bool = False,
) -> list[CapitalGainsFyPoint]:
    """List realised STCG/LTCG across every financial year with disposals.

    Args:
        investor_id: The ID of the investor.
        include_unreconciled: Whether to include unreconciled transactions.

    Returns:
        List of CapitalGainsFyPoint models summarizing capital gains by financial year.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params = {"include_unreconciled": str(include_unreconciled).lower()}
    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/reports/capital-gains-by-fy",
        params=params,
    )
    return [CapitalGainsFyPoint.model_validate(item) for item in data]

get async

get(investor_id: int, *, fy: str, include_unreconciled: bool = False) -> CapitalGainsReport

Get realised capital gains report for a specific financial year.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
fy str

Financial year string, e.g. "2024-25".

required
include_unreconciled bool

Whether to include unreconciled transactions.

False

Returns:

Type Description
CapitalGainsReport

CapitalGainsReport model with detailed lot-level gains.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(
    self,
    investor_id: int,
    *,
    fy: str,
    include_unreconciled: bool = False,
) -> CapitalGainsReport:
    """Get realised capital gains report for a specific financial year.

    Args:
        investor_id: The ID of the investor.
        fy: Financial year string, e.g. "2024-25".
        include_unreconciled: Whether to include unreconciled transactions.

    Returns:
        CapitalGainsReport model with detailed lot-level gains.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params = {
        "fy": fy,
        "include_unreconciled": str(include_unreconciled).lower(),
    }
    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/exports/capital-gains",
        params=params,
    )
    return CapitalGainsReport.model_validate(data)

FoliomanClient

Asynchronous client for interacting with the Folioman API.

Handles authentication, token refresh, and request execution.

Example
import asyncio
from folioman_client import FoliomanClient

async def main() -> None:
    async with FoliomanClient() as client:
        investor = await client.investors.get(1)
        summary = await client.portfolio.get(1)
        print(investor.name, summary.total_inr)

asyncio.run(main())
Source code in src/folioman_client/client.py
class FoliomanClient:
    """Asynchronous client for interacting with the Folioman API.

    Handles authentication, token refresh, and request execution.

    Example:
        ```python
        import asyncio
        from folioman_client import FoliomanClient

        async def main() -> None:
            async with FoliomanClient() as client:
                investor = await client.investors.get(1)
                summary = await client.portfolio.get(1)
                print(investor.name, summary.total_inr)

        asyncio.run(main())
        ```
    """

    def __init__(
        self,
        base_url: str | None = None,
        username: str | None = None,
        password: str | None = None,
        timeout: float = 30.0,
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        """Initialize FoliomanClient.

        Args:
            base_url: Base URL of the Folioman REST API. If None, loaded from settings/environment.
            username: Username for API authentication. If None, loaded from settings/environment.
            password: Password for API authentication. If None, loaded from settings/environment.
            timeout: Request timeout in seconds. Defaults to 30.0.
            http_client: Optional custom httpx.AsyncClient instance for custom transport/pooling.
        """
        self.base_url = (base_url or default_settings.base_url).rstrip("/")
        self.username = username if username is not None else default_settings.username
        self.password = password if password is not None else default_settings.password
        self.timeout = timeout

        self._auth = JWTAuthManager(
            base_url=self.base_url,
            username=self.username,
            password=self.password,
        )

        self._owns_http_client = http_client is None
        self._http_client = http_client or httpx.AsyncClient(
            base_url=self.base_url,
            timeout=self.timeout,
        )

        # Resource sub-clients
        self.investors = InvestorsResource(self)
        self.portfolio = PortfolioResource(self)
        self.holdings = HoldingsResource(self)
        self.transactions = TransactionsResource(self)
        self.valuations = ValuationsResource(self)
        self.capital_gains = CapitalGainsResource(self)

    @classmethod
    def from_settings(cls, settings: FoliomanSettings) -> FoliomanClient:
        """Create a client instance from a FoliomanSettings object.

        Args:
            settings: FoliomanSettings configuration object.

        Returns:
            A configured FoliomanClient instance.
        """
        return cls(
            base_url=settings.base_url,
            username=settings.username,
            password=settings.password,
            timeout=settings.timeout,
        )

    @classmethod
    def from_env(cls) -> FoliomanClient:
        """Create a client instance using environment variables.

        Returns:
            A configured FoliomanClient instance using environment defaults.
        """
        return cls.from_settings(FoliomanSettings())

    async def __aenter__(self) -> FoliomanClient:
        """Enter the async context manager.

        Returns:
            The FoliomanClient instance.
        """
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Exit the async context manager and close HTTP connections."""
        await self.close()

    async def close(self) -> None:
        """Close the underlying HTTP transport if owned by this client."""
        if self._owns_http_client:
            await self._http_client.aclose()

    def _normalize_path(self, path: str) -> str:
        """Ensure path starts with /api prefix as required by Folioman API.

        Args:
            path: Relative API path string.

        Returns:
            Normalized path starting with '/api/'.
        """
        path = path.strip()
        if not path.startswith("/"):
            path = f"/{path}"
        if not path.startswith("/api/"):
            path = f"/api{path}"
        return path

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: Any | None = None,
        headers: dict[str, str] | None = None,
        **kwargs: Any,
    ) -> Any:
        """Execute an authenticated HTTP request with automatic token refresh and 401 retry.

        Args:
            method: HTTP method ("GET", "POST", etc.)
            path: Relative API path (e.g. "/investors/1")
            params: Optional query parameters.
            json: Optional JSON request payload.
            headers: Optional extra headers.
            **kwargs: Additional keyword arguments passed to httpx.AsyncClient.request.

        Returns:
            Parsed JSON response, or None if status code is 204 or body is empty.

        Raises:
            FoliomanNotFoundError: If response is 404.
            FoliomanAuthError: If authentication fails or refresh is rejected.
            FoliomanAPIError: If server responds with other 4xx or 5xx status codes.
        """
        normalized_path = self._normalize_path(path)
        req_headers = dict(headers or {})

        # Obtain valid Bearer token
        token = await self._auth.get_valid_token(self._http_client)
        req_headers["Authorization"] = f"Bearer {token}"

        response = await self._http_client.request(
            method=method,
            url=normalized_path,
            params=params,
            json=json,
            headers=req_headers,
            **kwargs,
        )

        # Reactive refresh if 401 Unauthorized occurs
        if response.status_code == 401:
            new_token = await self._auth.force_refresh(self._http_client)
            req_headers["Authorization"] = f"Bearer {new_token}"
            response = await self._http_client.request(
                method=method,
                url=normalized_path,
                params=params,
                json=json,
                headers=req_headers,
                **kwargs,
            )

        return self._handle_response(response, normalized_path)

    def _handle_response(self, response: httpx.Response, path: str) -> Any:
        """Process HTTP response, translating errors to Folioman client exceptions.

        Args:
            response: httpx.Response object.
            path: Normalized path requested.

        Returns:
            Parsed response JSON or None.

        Raises:
            FoliomanNotFoundError: If response status is 404.
            FoliomanAuthError: If response status is 401.
            FoliomanAPIError: If response status is not successful or parsing fails.
        """
        if response.status_code == 404:
            raise FoliomanNotFoundError(f"Resource not found at {path}")

        if response.status_code == 401:
            raise FoliomanAuthError(f"Authentication failed for {path} (HTTP 401)")

        if not response.is_success:
            detail: Any = None
            try:
                data = response.json()
                detail = data.get("detail", data)
            except Exception:
                detail = response.text
            raise FoliomanAPIError(
                message=f"Folioman API error on {response.request.method} {path}: {detail}",
                status_code=response.status_code,
                response_data=detail,
            )

        if response.status_code == 204 or not response.content:
            return None

        try:
            return response.json()
        except Exception as exc:
            raise FoliomanAPIError(
                message=f"Failed to parse JSON response from {path}: {exc}",
                status_code=response.status_code,
                response_data=response.text,
            ) from exc

__init__

__init__(base_url: str | None = None, username: str | None = None, password: str | None = None, timeout: float = 30.0, http_client: AsyncClient | None = None) -> None

Initialize FoliomanClient.

Parameters:

Name Type Description Default
base_url str | None

Base URL of the Folioman REST API. If None, loaded from settings/environment.

None
username str | None

Username for API authentication. If None, loaded from settings/environment.

None
password str | None

Password for API authentication. If None, loaded from settings/environment.

None
timeout float

Request timeout in seconds. Defaults to 30.0.

30.0
http_client AsyncClient | None

Optional custom httpx.AsyncClient instance for custom transport/pooling.

None
Source code in src/folioman_client/client.py
def __init__(
    self,
    base_url: str | None = None,
    username: str | None = None,
    password: str | None = None,
    timeout: float = 30.0,
    http_client: httpx.AsyncClient | None = None,
) -> None:
    """Initialize FoliomanClient.

    Args:
        base_url: Base URL of the Folioman REST API. If None, loaded from settings/environment.
        username: Username for API authentication. If None, loaded from settings/environment.
        password: Password for API authentication. If None, loaded from settings/environment.
        timeout: Request timeout in seconds. Defaults to 30.0.
        http_client: Optional custom httpx.AsyncClient instance for custom transport/pooling.
    """
    self.base_url = (base_url or default_settings.base_url).rstrip("/")
    self.username = username if username is not None else default_settings.username
    self.password = password if password is not None else default_settings.password
    self.timeout = timeout

    self._auth = JWTAuthManager(
        base_url=self.base_url,
        username=self.username,
        password=self.password,
    )

    self._owns_http_client = http_client is None
    self._http_client = http_client or httpx.AsyncClient(
        base_url=self.base_url,
        timeout=self.timeout,
    )

    # Resource sub-clients
    self.investors = InvestorsResource(self)
    self.portfolio = PortfolioResource(self)
    self.holdings = HoldingsResource(self)
    self.transactions = TransactionsResource(self)
    self.valuations = ValuationsResource(self)
    self.capital_gains = CapitalGainsResource(self)

from_settings classmethod

from_settings(settings: FoliomanSettings) -> FoliomanClient

Create a client instance from a FoliomanSettings object.

Parameters:

Name Type Description Default
settings FoliomanSettings

FoliomanSettings configuration object.

required

Returns:

Type Description
FoliomanClient

A configured FoliomanClient instance.

Source code in src/folioman_client/client.py
@classmethod
def from_settings(cls, settings: FoliomanSettings) -> FoliomanClient:
    """Create a client instance from a FoliomanSettings object.

    Args:
        settings: FoliomanSettings configuration object.

    Returns:
        A configured FoliomanClient instance.
    """
    return cls(
        base_url=settings.base_url,
        username=settings.username,
        password=settings.password,
        timeout=settings.timeout,
    )

from_env classmethod

from_env() -> FoliomanClient

Create a client instance using environment variables.

Returns:

Type Description
FoliomanClient

A configured FoliomanClient instance using environment defaults.

Source code in src/folioman_client/client.py
@classmethod
def from_env(cls) -> FoliomanClient:
    """Create a client instance using environment variables.

    Returns:
        A configured FoliomanClient instance using environment defaults.
    """
    return cls.from_settings(FoliomanSettings())

__aenter__ async

__aenter__() -> FoliomanClient

Enter the async context manager.

Returns:

Type Description
FoliomanClient

The FoliomanClient instance.

Source code in src/folioman_client/client.py
async def __aenter__(self) -> FoliomanClient:
    """Enter the async context manager.

    Returns:
        The FoliomanClient instance.
    """
    return self

__aexit__ async

__aexit__(exc_type: Any, exc_val: Any, exc_tb: Any) -> None

Exit the async context manager and close HTTP connections.

Source code in src/folioman_client/client.py
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    """Exit the async context manager and close HTTP connections."""
    await self.close()

close async

close() -> None

Close the underlying HTTP transport if owned by this client.

Source code in src/folioman_client/client.py
async def close(self) -> None:
    """Close the underlying HTTP transport if owned by this client."""
    if self._owns_http_client:
        await self._http_client.aclose()

request async

request(method: str, path: str, *, params: dict[str, Any] | None = None, json: Any | None = None, headers: dict[str, str] | None = None, **kwargs: Any) -> Any

Execute an authenticated HTTP request with automatic token refresh and 401 retry.

Parameters:

Name Type Description Default
method str

HTTP method ("GET", "POST", etc.)

required
path str

Relative API path (e.g. "/investors/1")

required
params dict[str, Any] | None

Optional query parameters.

None
json Any | None

Optional JSON request payload.

None
headers dict[str, str] | None

Optional extra headers.

None
**kwargs Any

Additional keyword arguments passed to httpx.AsyncClient.request.

{}

Returns:

Type Description
Any

Parsed JSON response, or None if status code is 204 or body is empty.

Raises:

Type Description
FoliomanNotFoundError

If response is 404.

FoliomanAuthError

If authentication fails or refresh is rejected.

FoliomanAPIError

If server responds with other 4xx or 5xx status codes.

Source code in src/folioman_client/client.py
async def request(
    self,
    method: str,
    path: str,
    *,
    params: dict[str, Any] | None = None,
    json: Any | None = None,
    headers: dict[str, str] | None = None,
    **kwargs: Any,
) -> Any:
    """Execute an authenticated HTTP request with automatic token refresh and 401 retry.

    Args:
        method: HTTP method ("GET", "POST", etc.)
        path: Relative API path (e.g. "/investors/1")
        params: Optional query parameters.
        json: Optional JSON request payload.
        headers: Optional extra headers.
        **kwargs: Additional keyword arguments passed to httpx.AsyncClient.request.

    Returns:
        Parsed JSON response, or None if status code is 204 or body is empty.

    Raises:
        FoliomanNotFoundError: If response is 404.
        FoliomanAuthError: If authentication fails or refresh is rejected.
        FoliomanAPIError: If server responds with other 4xx or 5xx status codes.
    """
    normalized_path = self._normalize_path(path)
    req_headers = dict(headers or {})

    # Obtain valid Bearer token
    token = await self._auth.get_valid_token(self._http_client)
    req_headers["Authorization"] = f"Bearer {token}"

    response = await self._http_client.request(
        method=method,
        url=normalized_path,
        params=params,
        json=json,
        headers=req_headers,
        **kwargs,
    )

    # Reactive refresh if 401 Unauthorized occurs
    if response.status_code == 401:
        new_token = await self._auth.force_refresh(self._http_client)
        req_headers["Authorization"] = f"Bearer {new_token}"
        response = await self._http_client.request(
            method=method,
            url=normalized_path,
            params=params,
            json=json,
            headers=req_headers,
            **kwargs,
        )

    return self._handle_response(response, normalized_path)

HoldingsResource

Bases: _BaseResource

Endpoints for querying investor holdings and scheme details.

Source code in src/folioman_client/client.py
class HoldingsResource(_BaseResource):
    """Endpoints for querying investor holdings and scheme details."""

    async def list(
        self,
        investor_id: int,
        *,
        as_of: date | str | None = None,
    ) -> list[Holding]:
        """List priced holdings for an investor (extracted from portfolio summary).

        Args:
            investor_id: The ID of the investor.
            as_of: Optional point-in-time date.

        Returns:
            A list of Holding models.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        summary = await self._client.portfolio.get(investor_id, as_of=as_of)
        return summary.holdings

    async def get(
        self,
        investor_id: int,
        security_id: int,
        *,
        as_of: date | str | None = None,
    ) -> SchemeDetail:
        """Get detailed holding/scheme information (NAV history, transactions, folios).

        Args:
            investor_id: The ID of the investor.
            security_id: The ID of the security/scheme.
            as_of: Optional point-in-time valuation date.

        Returns:
            SchemeDetail model with full NAV points, folio balances, and transactions.

        Raises:
            FoliomanNotFoundError: If the investor or security does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {}
        if as_of is not None:
            params["as_of"] = (
                as_of.isoformat() if isinstance(as_of, date) else str(as_of)
            )

        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/holdings/{security_id}",
            params=params,
        )
        return SchemeDetail.model_validate(data)

list async

list(investor_id: int, *, as_of: date | str | None = None) -> list[Holding]

List priced holdings for an investor (extracted from portfolio summary).

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
as_of date | str | None

Optional point-in-time date.

None

Returns:

Type Description
list[Holding]

A list of Holding models.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    investor_id: int,
    *,
    as_of: date | str | None = None,
) -> list[Holding]:
    """List priced holdings for an investor (extracted from portfolio summary).

    Args:
        investor_id: The ID of the investor.
        as_of: Optional point-in-time date.

    Returns:
        A list of Holding models.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    summary = await self._client.portfolio.get(investor_id, as_of=as_of)
    return summary.holdings

get async

get(investor_id: int, security_id: int, *, as_of: date | str | None = None) -> SchemeDetail

Get detailed holding/scheme information (NAV history, transactions, folios).

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
security_id int

The ID of the security/scheme.

required
as_of date | str | None

Optional point-in-time valuation date.

None

Returns:

Type Description
SchemeDetail

SchemeDetail model with full NAV points, folio balances, and transactions.

Raises:

Type Description
FoliomanNotFoundError

If the investor or security does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(
    self,
    investor_id: int,
    security_id: int,
    *,
    as_of: date | str | None = None,
) -> SchemeDetail:
    """Get detailed holding/scheme information (NAV history, transactions, folios).

    Args:
        investor_id: The ID of the investor.
        security_id: The ID of the security/scheme.
        as_of: Optional point-in-time valuation date.

    Returns:
        SchemeDetail model with full NAV points, folio balances, and transactions.

    Raises:
        FoliomanNotFoundError: If the investor or security does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {}
    if as_of is not None:
        params["as_of"] = (
            as_of.isoformat() if isinstance(as_of, date) else str(as_of)
        )

    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/holdings/{security_id}",
        params=params,
    )
    return SchemeDetail.model_validate(data)

InvestorsResource

Bases: _BaseResource

Endpoints for managing and querying investors.

Source code in src/folioman_client/client.py
class InvestorsResource(_BaseResource):
    """Endpoints for managing and querying investors."""

    async def list(
        self,
        *,
        family_id: int | None = None,
        unaffiliated: bool = False,
    ) -> list[Investor]:
        """List investors accessible to the authenticated advisor.

        Args:
            family_id: Filter by parent family group ID.
            unaffiliated: If True, only returns investors not affiliated with any family.

        Returns:
            A list of Investor summary models.

        Raises:
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {}
        if family_id is not None:
            params["family_id"] = family_id
        if unaffiliated:
            params["unaffiliated"] = "true"

        data = await self._client.request("GET", "/investors/", params=params)
        return [Investor.model_validate(item) for item in data]

    async def get(self, investor_id: int) -> InvestorDetail:
        """Get investor details including masked PAN.

        Args:
            investor_id: Unique identifier of the investor.

        Returns:
            InvestorDetail model containing extended profile information.

        Raises:
            FoliomanNotFoundError: If the investor ID does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        data = await self._client.request("GET", f"/investors/{investor_id}")
        return InvestorDetail.model_validate(data)

list async

list(*, family_id: int | None = None, unaffiliated: bool = False) -> list[Investor]

List investors accessible to the authenticated advisor.

Parameters:

Name Type Description Default
family_id int | None

Filter by parent family group ID.

None
unaffiliated bool

If True, only returns investors not affiliated with any family.

False

Returns:

Type Description
list[Investor]

A list of Investor summary models.

Raises:

Type Description
FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    *,
    family_id: int | None = None,
    unaffiliated: bool = False,
) -> list[Investor]:
    """List investors accessible to the authenticated advisor.

    Args:
        family_id: Filter by parent family group ID.
        unaffiliated: If True, only returns investors not affiliated with any family.

    Returns:
        A list of Investor summary models.

    Raises:
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {}
    if family_id is not None:
        params["family_id"] = family_id
    if unaffiliated:
        params["unaffiliated"] = "true"

    data = await self._client.request("GET", "/investors/", params=params)
    return [Investor.model_validate(item) for item in data]

get async

get(investor_id: int) -> InvestorDetail

Get investor details including masked PAN.

Parameters:

Name Type Description Default
investor_id int

Unique identifier of the investor.

required

Returns:

Type Description
InvestorDetail

InvestorDetail model containing extended profile information.

Raises:

Type Description
FoliomanNotFoundError

If the investor ID does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(self, investor_id: int) -> InvestorDetail:
    """Get investor details including masked PAN.

    Args:
        investor_id: Unique identifier of the investor.

    Returns:
        InvestorDetail model containing extended profile information.

    Raises:
        FoliomanNotFoundError: If the investor ID does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    data = await self._client.request("GET", f"/investors/{investor_id}")
    return InvestorDetail.model_validate(data)

PortfolioResource

Bases: _BaseResource

Endpoints for investor portfolio summary and allocation.

Source code in src/folioman_client/client.py
class PortfolioResource(_BaseResource):
    """Endpoints for investor portfolio summary and allocation."""

    async def get(
        self,
        investor_id: int,
        *,
        as_of: date | str | None = None,
    ) -> PortfolioSummary:
        """Get the full portfolio summary, metrics, and asset mix for an investor.

        Args:
            investor_id: The ID of the investor.
            as_of: Optional point-in-time valuation date (YYYY-MM-DD or date object).

        Returns:
            PortfolioSummary model containing metrics, holdings, and allocation mixes.

        Raises:
            FoliomanNotFoundError: If the investor ID does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {}
        if as_of is not None:
            params["as_of"] = (
                as_of.isoformat() if isinstance(as_of, date) else str(as_of)
            )

        data = await self._client.request(
            "GET", f"/investors/{investor_id}/summary", params=params
        )
        return PortfolioSummary.model_validate(data)

get async

get(investor_id: int, *, as_of: date | str | None = None) -> PortfolioSummary

Get the full portfolio summary, metrics, and asset mix for an investor.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
as_of date | str | None

Optional point-in-time valuation date (YYYY-MM-DD or date object).

None

Returns:

Type Description
PortfolioSummary

PortfolioSummary model containing metrics, holdings, and allocation mixes.

Raises:

Type Description
FoliomanNotFoundError

If the investor ID does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(
    self,
    investor_id: int,
    *,
    as_of: date | str | None = None,
) -> PortfolioSummary:
    """Get the full portfolio summary, metrics, and asset mix for an investor.

    Args:
        investor_id: The ID of the investor.
        as_of: Optional point-in-time valuation date (YYYY-MM-DD or date object).

    Returns:
        PortfolioSummary model containing metrics, holdings, and allocation mixes.

    Raises:
        FoliomanNotFoundError: If the investor ID does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {}
    if as_of is not None:
        params["as_of"] = (
            as_of.isoformat() if isinstance(as_of, date) else str(as_of)
        )

    data = await self._client.request(
        "GET", f"/investors/{investor_id}/summary", params=params
    )
    return PortfolioSummary.model_validate(data)

TransactionsResource

Bases: _BaseResource

Endpoints for querying transaction ledger entries.

Source code in src/folioman_client/client.py
class TransactionsResource(_BaseResource):
    """Endpoints for querying transaction ledger entries."""

    async def list(self, investor_id: int) -> list[Transaction]:
        """List all transactions for an investor.

        Args:
            investor_id: The ID of the investor.

        Returns:
            List of Transaction ledger entries.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        data = await self._client.request(
            "GET", f"/investors/{investor_id}/transactions"
        )
        return [Transaction.model_validate(item) for item in data]

list async

list(investor_id: int) -> list[Transaction]

List all transactions for an investor.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required

Returns:

Type Description
list[Transaction]

List of Transaction ledger entries.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(self, investor_id: int) -> list[Transaction]:
    """List all transactions for an investor.

    Args:
        investor_id: The ID of the investor.

    Returns:
        List of Transaction ledger entries.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    data = await self._client.request(
        "GET", f"/investors/{investor_id}/transactions"
    )
    return [Transaction.model_validate(item) for item in data]

ValuationsResource

Bases: _BaseResource

Endpoints for portfolio net-worth history and valuation status.

Source code in src/folioman_client/client.py
class ValuationsResource(_BaseResource):
    """Endpoints for portfolio net-worth history and valuation status."""

    async def list(
        self,
        investor_id: int,
        *,
        from_date: date | str | None = None,
        to_date: date | str | None = None,
        granularity: Literal["daily", "weekly", "monthly"] = "monthly",
    ) -> ValueSeries:
        """Get net-worth time series reconstructed from ledger and NAV history.

        Args:
            investor_id: The ID of the investor.
            from_date: Start date of series.
            to_date: End date of series.
            granularity: Sampling frequency ('daily', 'weekly', or 'monthly').

        Returns:
            ValueSeries model containing historical valuation points.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {"granularity": granularity}
        if from_date is not None:
            params["from"] = (
                from_date.isoformat() if isinstance(from_date, date) else str(from_date)
            )
        if to_date is not None:
            params["to"] = (
                to_date.isoformat() if isinstance(to_date, date) else str(to_date)
            )

        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/value-series",
            params=params,
        )
        return ValueSeries.model_validate(data)

    async def status(self, investor_id: int) -> ValuationStatus:
        """Get the current valuation calculation readiness status for an investor.

        Args:
            investor_id: The ID of the investor.

        Returns:
            ValuationStatus model indicating calculation state and coverage.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        data = await self._client.request(
            "GET", f"/investors/{investor_id}/valuation-status"
        )
        return ValuationStatus.model_validate(data)

list async

list(investor_id: int, *, from_date: date | str | None = None, to_date: date | str | None = None, granularity: Literal['daily', 'weekly', 'monthly'] = 'monthly') -> ValueSeries

Get net-worth time series reconstructed from ledger and NAV history.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
from_date date | str | None

Start date of series.

None
to_date date | str | None

End date of series.

None
granularity Literal['daily', 'weekly', 'monthly']

Sampling frequency ('daily', 'weekly', or 'monthly').

'monthly'

Returns:

Type Description
ValueSeries

ValueSeries model containing historical valuation points.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    investor_id: int,
    *,
    from_date: date | str | None = None,
    to_date: date | str | None = None,
    granularity: Literal["daily", "weekly", "monthly"] = "monthly",
) -> ValueSeries:
    """Get net-worth time series reconstructed from ledger and NAV history.

    Args:
        investor_id: The ID of the investor.
        from_date: Start date of series.
        to_date: End date of series.
        granularity: Sampling frequency ('daily', 'weekly', or 'monthly').

    Returns:
        ValueSeries model containing historical valuation points.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {"granularity": granularity}
    if from_date is not None:
        params["from"] = (
            from_date.isoformat() if isinstance(from_date, date) else str(from_date)
        )
    if to_date is not None:
        params["to"] = (
            to_date.isoformat() if isinstance(to_date, date) else str(to_date)
        )

    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/value-series",
        params=params,
    )
    return ValueSeries.model_validate(data)

status async

status(investor_id: int) -> ValuationStatus

Get the current valuation calculation readiness status for an investor.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required

Returns:

Type Description
ValuationStatus

ValuationStatus model indicating calculation state and coverage.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def status(self, investor_id: int) -> ValuationStatus:
    """Get the current valuation calculation readiness status for an investor.

    Args:
        investor_id: The ID of the investor.

    Returns:
        ValuationStatus model indicating calculation state and coverage.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    data = await self._client.request(
        "GET", f"/investors/{investor_id}/valuation-status"
    )
    return ValuationStatus.model_validate(data)

FoliomanSettings

Bases: BaseSettings

Folioman client configuration backed by environment variables.

Attributes:

Name Type Description
base_url str

The base URL of the Folioman REST API service. Defaults to "http://localhost:8000".

username str

The username used for HTTP basic or JWT token retrieval. Defaults to "".

password str

The password used for HTTP basic or JWT token retrieval. Defaults to "".

timeout float

The request timeout in seconds. Defaults to 30.0.

Source code in src/folioman_client/config.py
class FoliomanSettings(BaseSettings):
    """Folioman client configuration backed by environment variables.

    Attributes:
        base_url: The base URL of the Folioman REST API service.
            Defaults to "http://localhost:8000".
        username: The username used for HTTP basic or JWT token retrieval.
            Defaults to "".
        password: The password used for HTTP basic or JWT token retrieval.
            Defaults to "".
        timeout: The request timeout in seconds.
            Defaults to 30.0.
    """

    model_config = SettingsConfigDict(
        env_prefix="FOLIOMAN_",
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    base_url: str = Field(
        default="http://localhost:8000",
        description="Base URL of the Folioman REST API service.",
    )
    username: str = Field(
        default="",
        description="Username or advisor identifier for authentication.",
    )
    password: str = Field(
        default="",
        description="Password or secret credential for authentication.",
    )
    timeout: float = Field(
        default=30.0,
        description="HTTP request timeout in seconds.",
    )

    @property
    def folioman_url(self) -> str:
        """Alias for base_url."""
        return self.base_url

    @property
    def folioman_username(self) -> str:
        """Alias for username."""
        return self.username

    @property
    def folioman_password(self) -> str:
        """Alias for password."""
        return self.password

folioman_url property

folioman_url: str

Alias for base_url.

folioman_username property

folioman_username: str

Alias for username.

folioman_password property

folioman_password: str

Alias for password.

FoliomanAPIError

Bases: FoliomanError

Raised when the Folioman API returns an error response (HTTP 4xx/5xx).

Attributes:

Name Type Description
status_code

The HTTP status code returned by the server.

response_data

Parsed response payload or raw error details, if available.

Source code in src/folioman_client/errors.py
class FoliomanAPIError(FoliomanError):
    """Raised when the Folioman API returns an error response (HTTP 4xx/5xx).

    Attributes:
        status_code: The HTTP status code returned by the server.
        response_data: Parsed response payload or raw error details, if available.
    """

    def __init__(
        self,
        message: str,
        status_code: int,
        response_data: Any | None = None,
    ) -> None:
        """Initialize FoliomanAPIError with message, status code, and optional payload.

        Args:
            message: Human-readable description of the error.
            status_code: HTTP response status code (e.g., 400, 500).
            response_data: Deserialized JSON payload or raw text error from the server.
        """
        super().__init__(message)
        self.status_code = status_code
        self.response_data = response_data

    def __str__(self) -> str:
        base = super().__str__()
        if self.response_data:
            return f"[{self.status_code}] {base} - {self.response_data}"
        return f"[{self.status_code}] {base}"

__init__

__init__(message: str, status_code: int, response_data: Any | None = None) -> None

Initialize FoliomanAPIError with message, status code, and optional payload.

Parameters:

Name Type Description Default
message str

Human-readable description of the error.

required
status_code int

HTTP response status code (e.g., 400, 500).

required
response_data Any | None

Deserialized JSON payload or raw text error from the server.

None
Source code in src/folioman_client/errors.py
def __init__(
    self,
    message: str,
    status_code: int,
    response_data: Any | None = None,
) -> None:
    """Initialize FoliomanAPIError with message, status code, and optional payload.

    Args:
        message: Human-readable description of the error.
        status_code: HTTP response status code (e.g., 400, 500).
        response_data: Deserialized JSON payload or raw text error from the server.
    """
    super().__init__(message)
    self.status_code = status_code
    self.response_data = response_data

FoliomanAuthError

Bases: FoliomanError

Raised when authentication fails (invalid credentials, expired/rejected token refresh).

Source code in src/folioman_client/errors.py
class FoliomanAuthError(FoliomanError):
    """Raised when authentication fails (invalid credentials, expired/rejected token refresh)."""

FoliomanError

Bases: Exception

Base exception for all Folioman client errors.

Source code in src/folioman_client/errors.py
class FoliomanError(Exception):
    """Base exception for all Folioman client errors."""

FoliomanNotFoundError

Bases: FoliomanError

Raised when the requested resource is not found (HTTP 404).

Source code in src/folioman_client/errors.py
class FoliomanNotFoundError(FoliomanError):
    """Raised when the requested resource is not found (HTTP 404)."""

AccessToken

Bases: FoliomanBaseModel

Refreshed access token.

Attributes:

Name Type Description
access str

Newly minted short-lived JWT bearer token.

Source code in src/folioman_client/models.py
class AccessToken(FoliomanBaseModel):
    """Refreshed access token.

    Attributes:
        access: Newly minted short-lived JWT bearer token.
    """

    access: str

AllocationBucket

Bases: FoliomanBaseModel

Allocation breakdown row by AMC or category.

Attributes:

Name Type Description
label str

AMC name or category classification label.

value_inr ConfiguredDecimal

Total valuation allocated to this bucket in INR.

Source code in src/folioman_client/models.py
class AllocationBucket(FoliomanBaseModel):
    """Allocation breakdown row by AMC or category.

    Attributes:
        label: AMC name or category classification label.
        value_inr: Total valuation allocated to this bucket in INR.
    """

    label: str
    value_inr: ConfiguredDecimal

AssetMixRow

Bases: FoliomanBaseModel

Allocation breakdown row by security type.

Attributes:

Name Type Description
security_type str

Asset class label (e.g. 'EQUITY', 'DEBT', 'CASH').

value_inr ConfiguredDecimal

Total valuation allocated to this security type in INR.

Source code in src/folioman_client/models.py
class AssetMixRow(FoliomanBaseModel):
    """Allocation breakdown row by security type.

    Attributes:
        security_type: Asset class label (e.g. 'EQUITY', 'DEBT', 'CASH').
        value_inr: Total valuation allocated to this security type in INR.
    """

    security_type: str
    value_inr: ConfiguredDecimal

CapitalGainRow

Bases: FoliomanBaseModel

One realised disposal lot in capital gains report.

Attributes:

Name Type Description
security_id int | None

ID of the security redeemed or sold.

name str

Name of the security or mutual fund scheme.

isin str

ISIN code of the security.

units ConfiguredDecimal

Number of units redeemed or disposed.

sale_value ConfiguredDecimal

Realized sale proceeds in INR.

cost ConfiguredDecimal

Indexed or purchase cost basis in INR.

gain ConfiguredDecimal

Realized capital gain or loss in INR.

term str

Classification of gain ('STCG' or 'LTCG').

acquired_on ConfiguredDate

Original purchase date of the lot.

sold_on ConfiguredDate

Date of disposal or redemption.

grandfathering_unavailable bool

Whether Section 112A grandfathering is unavailable.

Source code in src/folioman_client/models.py
class CapitalGainRow(FoliomanBaseModel):
    """One realised disposal lot in capital gains report.

    Attributes:
        security_id: ID of the security redeemed or sold.
        name: Name of the security or mutual fund scheme.
        isin: ISIN code of the security.
        units: Number of units redeemed or disposed.
        sale_value: Realized sale proceeds in INR.
        cost: Indexed or purchase cost basis in INR.
        gain: Realized capital gain or loss in INR.
        term: Classification of gain ('STCG' or 'LTCG').
        acquired_on: Original purchase date of the lot.
        sold_on: Date of disposal or redemption.
        grandfathering_unavailable: Whether Section 112A grandfathering is unavailable.
    """

    security_id: int | None = None
    name: str
    isin: str = ""
    units: ConfiguredDecimal
    sale_value: ConfiguredDecimal
    cost: ConfiguredDecimal
    gain: ConfiguredDecimal
    term: str
    acquired_on: ConfiguredDate
    sold_on: ConfiguredDate
    grandfathering_unavailable: bool = False

CapitalGainsFyPoint

Bases: FoliomanBaseModel

Year-over-year capital gains summary point.

Attributes:

Name Type Description
fy str

Financial year label (e.g., '2023-24').

stcg ConfiguredDecimal

Total realized STCG for the year.

ltcg ConfiguredDecimal

Total realized LTCG for the year.

Source code in src/folioman_client/models.py
class CapitalGainsFyPoint(FoliomanBaseModel):
    """Year-over-year capital gains summary point.

    Attributes:
        fy: Financial year label (e.g., '2023-24').
        stcg: Total realized STCG for the year.
        ltcg: Total realized LTCG for the year.
    """

    fy: str
    stcg: ConfiguredDecimal
    ltcg: ConfiguredDecimal

CapitalGainsReport

Bases: FoliomanBaseModel

Realised capital gains report for a financial year (CapitalGainsOut).

Attributes:

Name Type Description
fy str

Financial year label (e.g., '2024-25').

stcg_total ConfiguredDecimal

Total realized Short-Term Capital Gains in INR.

ltcg_total ConfiguredDecimal

Total realized Long-Term Capital Gains in INR.

rows list[CapitalGainRow]

Detailed breakdown of individual disposal lots.

disclaimer str

Legal or regulatory tax disclaimer text.

Source code in src/folioman_client/models.py
class CapitalGainsReport(FoliomanBaseModel):
    """Realised capital gains report for a financial year (CapitalGainsOut).

    Attributes:
        fy: Financial year label (e.g., '2024-25').
        stcg_total: Total realized Short-Term Capital Gains in INR.
        ltcg_total: Total realized Long-Term Capital Gains in INR.
        rows: Detailed breakdown of individual disposal lots.
        disclaimer: Legal or regulatory tax disclaimer text.
    """

    fy: str
    stcg_total: ConfiguredDecimal
    ltcg_total: ConfiguredDecimal
    rows: list[CapitalGainRow] = Field(default_factory=list)
    disclaimer: str = ""

FolioBalance

Bases: FoliomanBaseModel

Balance for one folio holding a security.

Attributes:

Name Type Description
number str

Folio account number.

broker str

Broker / ARN identifier associated with the folio.

folio_type str

Type classification of the folio account.

units ConfiguredDecimal

Unit balance held under this folio.

value_inr ConfiguredDecimal | None

Monetary valuation in INR for this folio.

Source code in src/folioman_client/models.py
class FolioBalance(FoliomanBaseModel):
    """Balance for one folio holding a security.

    Attributes:
        number: Folio account number.
        broker: Broker / ARN identifier associated with the folio.
        folio_type: Type classification of the folio account.
        units: Unit balance held under this folio.
        value_inr: Monetary valuation in INR for this folio.
    """

    number: str
    broker: str = ""
    folio_type: str = ""
    units: ConfiguredDecimal
    value_inr: ConfiguredDecimal | None = None

FoliomanBaseModel

Bases: BaseModel

Base model with common configuration for all Folioman models.

Ignores extra keys sent by the API for forward compatibility and supports field population by name.

Source code in src/folioman_client/models.py
class FoliomanBaseModel(BaseModel):
    """Base model with common configuration for all Folioman models.

    Ignores extra keys sent by the API for forward compatibility and
    supports field population by name.
    """

    model_config = ConfigDict(
        extra="ignore",
        populate_by_name=True,
    )

Holding

Bases: FoliomanBaseModel

Priced holding row under an investor.

Attributes:

Name Type Description
security_id int

Unique identifier for the underlying security/scheme.

name str

Name of the security or mutual fund scheme.

security_type str

Category of the security (e.g., 'MF', 'EQUITY').

symbol str

Ticker symbol or trading identifier.

amc str

Asset Management Company name.

category str

SEBI category or mutual fund classification.

units ConfiguredDecimal

Total quantity of units held.

value_inr ConfiguredDecimal | None

Current market valuation in INR.

invested_inr ConfiguredDecimal | None

Total invested amount (cost basis) in INR.

latest_nav ConfiguredDecimal | None

Latest available Net Asset Value.

return_pct float | None

Absolute percentage return.

xirr float | None

Extended Internal Rate of Return (annualized).

day_change_inr ConfiguredDecimal | None

Monetary change in valuation since the previous trading day.

day_change_pct float | None

Percentage change since the previous trading day.

Source code in src/folioman_client/models.py
class Holding(FoliomanBaseModel):
    """Priced holding row under an investor.

    Attributes:
        security_id: Unique identifier for the underlying security/scheme.
        name: Name of the security or mutual fund scheme.
        security_type: Category of the security (e.g., 'MF', 'EQUITY').
        symbol: Ticker symbol or trading identifier.
        amc: Asset Management Company name.
        category: SEBI category or mutual fund classification.
        units: Total quantity of units held.
        value_inr: Current market valuation in INR.
        invested_inr: Total invested amount (cost basis) in INR.
        latest_nav: Latest available Net Asset Value.
        return_pct: Absolute percentage return.
        xirr: Extended Internal Rate of Return (annualized).
        day_change_inr: Monetary change in valuation since the previous trading day.
        day_change_pct: Percentage change since the previous trading day.
    """

    security_id: int
    name: str
    security_type: str
    symbol: str = ""
    amc: str = ""
    category: str = ""
    units: ConfiguredDecimal
    value_inr: ConfiguredDecimal | None = None
    invested_inr: ConfiguredDecimal | None = None
    latest_nav: ConfiguredDecimal | None = None
    return_pct: float | None = None
    xirr: float | None = None
    day_change_inr: ConfiguredDecimal | None = None
    day_change_pct: float | None = None

Investor

Bases: FoliomanBaseModel

Investor summary representation.

Attributes:

Name Type Description
id int

Unique numeric identifier of the investor.

name str

Full legal name or display name of the investor.

email str

Contact email address of the investor.

is_huf bool

Whether the investor represents a Hindu Undivided Family.

relation str

Relationship description if part of a family group.

family_id int | None

ID of the parent family group, if affiliated.

has_pan bool

Whether a Permanent Account Number is registered.

pan_locked bool

Whether PAN changes are locked for compliance.

created_at ConfiguredDatetime | None

Timestamp when the investor record was created.

updated_at ConfiguredDatetime | None

Timestamp when the investor record was last updated.

Source code in src/folioman_client/models.py
class Investor(FoliomanBaseModel):
    """Investor summary representation.

    Attributes:
        id: Unique numeric identifier of the investor.
        name: Full legal name or display name of the investor.
        email: Contact email address of the investor.
        is_huf: Whether the investor represents a Hindu Undivided Family.
        relation: Relationship description if part of a family group.
        family_id: ID of the parent family group, if affiliated.
        has_pan: Whether a Permanent Account Number is registered.
        pan_locked: Whether PAN changes are locked for compliance.
        created_at: Timestamp when the investor record was created.
        updated_at: Timestamp when the investor record was last updated.
    """

    id: int
    name: str
    email: str = ""
    is_huf: bool = False
    relation: str = ""
    family_id: int | None = None
    has_pan: bool = False
    pan_locked: bool = False
    created_at: ConfiguredDatetime | None = None
    updated_at: ConfiguredDatetime | None = None

InvestorDetail

Bases: Investor

Investor detailed representation with masked PAN.

Attributes:

Name Type Description
pan_masked str

Masked PAN string (e.g. 'ABCDE****F') protecting sensitive PII.

Source code in src/folioman_client/models.py
class InvestorDetail(Investor):
    """Investor detailed representation with masked PAN.

    Attributes:
        pan_masked: Masked PAN string (e.g. 'ABCDE****F') protecting sensitive PII.
    """

    pan_masked: str = ""

NavPoint

Bases: FoliomanBaseModel

Single date and NAV point.

Attributes:

Name Type Description
date ConfiguredDate

Valuation date for the NAV point.

nav ConfiguredDecimal

Net Asset Value per unit on the specified date.

Source code in src/folioman_client/models.py
class NavPoint(FoliomanBaseModel):
    """Single date and NAV point.

    Attributes:
        date: Valuation date for the NAV point.
        nav: Net Asset Value per unit on the specified date.
    """

    date: ConfiguredDate
    nav: ConfiguredDecimal

PeriodReturn

Bases: FoliomanBaseModel

Trailing window money-weighted return (1M, 1Y, All, etc.).

Attributes:

Name Type Description
period str

Label for the trailing window (e.g. '1M', '3M', '1Y', 'ALL').

annualized float

Annualized internal rate of return for the period.

absolute float | None

Absolute percentage return for the period.

days int

Number of calendar days in the evaluation window.

Source code in src/folioman_client/models.py
class PeriodReturn(FoliomanBaseModel):
    """Trailing window money-weighted return (1M, 1Y, All, etc.).

    Attributes:
        period: Label for the trailing window (e.g. '1M', '3M', '1Y', 'ALL').
        annualized: Annualized internal rate of return for the period.
        absolute: Absolute percentage return for the period.
        days: Number of calendar days in the evaluation window.
    """

    period: str
    annualized: float
    absolute: float | None = None
    days: int

PortfolioSummary

Bases: FoliomanBaseModel

Overall portfolio summary for an investor (InvestorSummaryOut).

Attributes:

Name Type Description
investor_id int

Unique identifier of the investor.

as_of ConfiguredDate

Valuation date of the portfolio summary.

total_inr ConfiguredDecimal

Aggregate portfolio valuation in INR.

is_provisional bool

Flag indicating if pricing is provisional or final.

navs_as_of ConfiguredDate | None

Effective date of NAV points used in this valuation.

navs_stale bool

True if latest NAVs have not been updated recently.

holdings_count int

Total count of active holdings.

integrity_unit_count int

Count of holdings with verified unit balances.

tax_ready_count int

Count of holdings with reconciled tax lots.

needs_attention_count int

Holdings requiring advisor intervention.

snapshot_count int

Number of historical snapshots available.

stale_count int

Number of unpriced or stale holdings.

unpriced_fund_count int

Number of holdings without available NAV.

last_import_at ConfiguredDatetime | None

Timestamp of the most recent data import.

day_change_inr ConfiguredDecimal | None

Monetary change since previous business day.

xirr float | None

Overall portfolio annualized internal rate of return.

period_returns list[PeriodReturn]

Trailing performance metrics across windows.

asset_mix list[AssetMixRow]

Asset class breakdown (Equity, Debt, Cash, etc.).

amc_mix list[AllocationBucket]

Asset Management Company distribution breakdown.

category_mix list[AllocationBucket]

Mutual fund category distribution breakdown.

top_holdings list[Holding]

Subset of top holdings by value.

holdings list[Holding]

Full list of priced holdings for this investor.

Source code in src/folioman_client/models.py
class PortfolioSummary(FoliomanBaseModel):
    """Overall portfolio summary for an investor (InvestorSummaryOut).

    Attributes:
        investor_id: Unique identifier of the investor.
        as_of: Valuation date of the portfolio summary.
        total_inr: Aggregate portfolio valuation in INR.
        is_provisional: Flag indicating if pricing is provisional or final.
        navs_as_of: Effective date of NAV points used in this valuation.
        navs_stale: True if latest NAVs have not been updated recently.
        holdings_count: Total count of active holdings.
        integrity_unit_count: Count of holdings with verified unit balances.
        tax_ready_count: Count of holdings with reconciled tax lots.
        needs_attention_count: Holdings requiring advisor intervention.
        snapshot_count: Number of historical snapshots available.
        stale_count: Number of unpriced or stale holdings.
        unpriced_fund_count: Number of holdings without available NAV.
        last_import_at: Timestamp of the most recent data import.
        day_change_inr: Monetary change since previous business day.
        xirr: Overall portfolio annualized internal rate of return.
        period_returns: Trailing performance metrics across windows.
        asset_mix: Asset class breakdown (Equity, Debt, Cash, etc.).
        amc_mix: Asset Management Company distribution breakdown.
        category_mix: Mutual fund category distribution breakdown.
        top_holdings: Subset of top holdings by value.
        holdings: Full list of priced holdings for this investor.
    """

    investor_id: int
    as_of: ConfiguredDate
    total_inr: ConfiguredDecimal
    is_provisional: bool = False
    navs_as_of: ConfiguredDate | None = None
    navs_stale: bool = False
    holdings_count: int = 0
    integrity_unit_count: int = 0
    tax_ready_count: int = 0
    needs_attention_count: int = 0
    snapshot_count: int = 0
    stale_count: int = 0
    unpriced_fund_count: int = 0
    last_import_at: ConfiguredDatetime | None = None
    day_change_inr: ConfiguredDecimal | None = None
    xirr: float | None = None
    period_returns: list[PeriodReturn] = Field(default_factory=list)
    asset_mix: list[AssetMixRow] = Field(default_factory=list)
    amc_mix: list[AllocationBucket] = Field(default_factory=list)
    category_mix: list[AllocationBucket] = Field(default_factory=list)
    top_holdings: list[Holding] = Field(default_factory=list)
    holdings: list[Holding] = Field(default_factory=list)

SchemeDetail

Bases: FoliomanBaseModel

Detailed scheme view for an investor.

Attributes:

Name Type Description
security SchemeRef

Scheme metadata reference.

as_of ConfiguredDate

Point-in-time calculation date.

units ConfiguredDecimal

Total units held across all folios.

value_inr ConfiguredDecimal | None

Current market value in INR.

invested_inr ConfiguredDecimal | None

Total cost basis in INR.

return_pct float | None

Absolute return percentage.

xirr float | None

Annualized internal rate of return.

xirr_status str

Status indicator for XIRR calculation convergence.

day_change_inr ConfiguredDecimal | None

Valuation change compared to previous trading day.

day_change_pct float | None

Percentage change compared to previous trading day.

latest_nav ConfiguredDecimal | None

Most recent recorded Net Asset Value.

latest_nav_date ConfiguredDate | None

Date of the latest NAV record.

has_transactions bool

Whether transaction records are available.

partial_history bool

Whether historical records are incomplete.

partial_history_from ConfiguredDate | None

Starting date of available history if partial.

folios list[FolioBalance]

Breakdown of units across individual folios.

nav_history list[NavPoint]

Time series of historical NAV points.

transactions list[Transaction]

Ledger of historical transactions for this scheme.

Source code in src/folioman_client/models.py
class SchemeDetail(FoliomanBaseModel):
    """Detailed scheme view for an investor.

    Attributes:
        security: Scheme metadata reference.
        as_of: Point-in-time calculation date.
        units: Total units held across all folios.
        value_inr: Current market value in INR.
        invested_inr: Total cost basis in INR.
        return_pct: Absolute return percentage.
        xirr: Annualized internal rate of return.
        xirr_status: Status indicator for XIRR calculation convergence.
        day_change_inr: Valuation change compared to previous trading day.
        day_change_pct: Percentage change compared to previous trading day.
        latest_nav: Most recent recorded Net Asset Value.
        latest_nav_date: Date of the latest NAV record.
        has_transactions: Whether transaction records are available.
        partial_history: Whether historical records are incomplete.
        partial_history_from: Starting date of available history if partial.
        folios: Breakdown of units across individual folios.
        nav_history: Time series of historical NAV points.
        transactions: Ledger of historical transactions for this scheme.
    """

    security: SchemeRef
    as_of: ConfiguredDate
    units: ConfiguredDecimal
    value_inr: ConfiguredDecimal | None = None
    invested_inr: ConfiguredDecimal | None = None
    return_pct: float | None = None
    xirr: float | None = None
    xirr_status: str = ""
    day_change_inr: ConfiguredDecimal | None = None
    day_change_pct: float | None = None
    latest_nav: ConfiguredDecimal | None = None
    latest_nav_date: ConfiguredDate | None = None
    has_transactions: bool = False
    partial_history: bool = False
    partial_history_from: ConfiguredDate | None = None
    folios: list[FolioBalance] = Field(default_factory=list)
    nav_history: list[NavPoint] = Field(default_factory=list)
    transactions: list[Transaction] = Field(default_factory=list)

SchemeRef

Bases: FoliomanBaseModel

Security identity metadata.

Attributes:

Name Type Description
id int

Unique identifier of the security.

name str

Full name of the mutual fund scheme or security.

isin str

International Securities Identification Number.

symbol str

Ticker symbol if traded on an exchange.

security_type str

Type of security (e.g. 'MF', 'EQUITY').

amfi_code str

Association of Mutual Funds in India identifier.

amc str | None

Asset Management Company managing the scheme.

category str | None

Scheme investment category.

Source code in src/folioman_client/models.py
class SchemeRef(FoliomanBaseModel):
    """Security identity metadata.

    Attributes:
        id: Unique identifier of the security.
        name: Full name of the mutual fund scheme or security.
        isin: International Securities Identification Number.
        symbol: Ticker symbol if traded on an exchange.
        security_type: Type of security (e.g. 'MF', 'EQUITY').
        amfi_code: Association of Mutual Funds in India identifier.
        amc: Asset Management Company managing the scheme.
        category: Scheme investment category.
    """

    id: int
    name: str
    isin: str = ""
    symbol: str = ""
    security_type: str = ""
    amfi_code: str = ""
    amc: str | None = None
    category: str | None = None

TokenPair

Bases: FoliomanBaseModel

Access and refresh token pair returned on authentication.

Attributes:

Name Type Description
access str

Short-lived JWT bearer token used for authorizing requests.

refresh str

Long-lived refresh token used to obtain renewed access tokens.

Source code in src/folioman_client/models.py
class TokenPair(FoliomanBaseModel):
    """Access and refresh token pair returned on authentication.

    Attributes:
        access: Short-lived JWT bearer token used for authorizing requests.
        refresh: Long-lived refresh token used to obtain renewed access tokens.
    """

    access: str
    refresh: str

Transaction

Bases: FoliomanBaseModel

Transaction ledger record.

Attributes:

Name Type Description
id int

Unique numeric identifier for the transaction record.

investor_id int

ID of the investor who owns the holding.

security_id int

ID of the security being traded.

folio_id int | None

ID of the folio account if linked.

date ConfiguredDate

Effective transaction date.

transaction_type str

Transaction category (e.g., 'PURCHASE', 'REDEMPTION', 'SIP').

units ConfiguredDecimal

Number of units transacted.

nav_or_price ConfiguredDecimal

Unit price or NAV at which the transaction was executed.

amount ConfiguredDecimal | None

Gross transaction amount in INR.

fees ConfiguredDecimal

Fees associated with the transaction.

stamp_duty ConfiguredDecimal

Mandatory stamp duty charges.

brokerage ConfiguredDecimal

Brokerage commission charged.

currency str

ISO currency code (defaults to 'INR').

source str

Ingestion source or platform (e.g., 'CAMS', 'KFintech').

narration str

Descriptive ledger text or note.

cost_basis_complete bool

Flag indicating whether cost basis is known.

via_security str | None

Auxiliary security reference for switches.

balance ConfiguredDecimal | None

Cumulative unit balance following this transaction.

Source code in src/folioman_client/models.py
class Transaction(FoliomanBaseModel):
    """Transaction ledger record.

    Attributes:
        id: Unique numeric identifier for the transaction record.
        investor_id: ID of the investor who owns the holding.
        security_id: ID of the security being traded.
        folio_id: ID of the folio account if linked.
        date: Effective transaction date.
        transaction_type: Transaction category (e.g., 'PURCHASE', 'REDEMPTION', 'SIP').
        units: Number of units transacted.
        nav_or_price: Unit price or NAV at which the transaction was executed.
        amount: Gross transaction amount in INR.
        fees: Fees associated with the transaction.
        stamp_duty: Mandatory stamp duty charges.
        brokerage: Brokerage commission charged.
        currency: ISO currency code (defaults to 'INR').
        source: Ingestion source or platform (e.g., 'CAMS', 'KFintech').
        narration: Descriptive ledger text or note.
        cost_basis_complete: Flag indicating whether cost basis is known.
        via_security: Auxiliary security reference for switches.
        balance: Cumulative unit balance following this transaction.
    """

    id: int
    investor_id: int
    security_id: int
    folio_id: int | None = None
    date: ConfiguredDate
    transaction_type: str
    units: ConfiguredDecimal
    nav_or_price: ConfiguredDecimal
    amount: ConfiguredDecimal | None = None
    fees: ConfiguredDecimal = Decimal("0")
    stamp_duty: ConfiguredDecimal = Decimal("0")
    brokerage: ConfiguredDecimal = Decimal("0")
    currency: str = "INR"
    source: str = ""
    narration: str = ""
    cost_basis_complete: bool = True
    via_security: str | None = None
    balance: ConfiguredDecimal | None = None

ValuationStatus

Bases: FoliomanBaseModel

Valuation calculation readiness status.

Attributes:

Name Type Description
investor_id int | None

ID of the investor.

family_id int | None

ID of the family group if applicable.

status str

Engine readiness status (e.g. 'READY', 'COMPUTING', 'ERROR').

computed_through ConfiguredDate | None

Date up to which valuations have been finalized.

recompute_from ConfiguredDate | None

Earliest date from which recalculation is needed.

is_provisional bool

Whether the current numbers are provisional.

Source code in src/folioman_client/models.py
class ValuationStatus(FoliomanBaseModel):
    """Valuation calculation readiness status.

    Attributes:
        investor_id: ID of the investor.
        family_id: ID of the family group if applicable.
        status: Engine readiness status (e.g. 'READY', 'COMPUTING', 'ERROR').
        computed_through: Date up to which valuations have been finalized.
        recompute_from: Earliest date from which recalculation is needed.
        is_provisional: Whether the current numbers are provisional.
    """

    investor_id: int | None = None
    family_id: int | None = None
    status: str
    computed_through: ConfiguredDate | None = None
    recompute_from: ConfiguredDate | None = None
    is_provisional: bool = False

ValueSeries

Bases: FoliomanBaseModel

Reconstructed net-worth-over-time time series.

Attributes:

Name Type Description
investor_id int | None

Optional investor ID filter.

family_id int | None

Optional family ID filter.

start ConfiguredDate

Start date of the time series.

end ConfiguredDate

End date of the time series.

granularity str

Sampling frequency ('daily', 'weekly', 'monthly').

points list[ValueSeriesPoint]

Ordered list of valuation time series points.

Source code in src/folioman_client/models.py
class ValueSeries(FoliomanBaseModel):
    """Reconstructed net-worth-over-time time series.

    Attributes:
        investor_id: Optional investor ID filter.
        family_id: Optional family ID filter.
        start: Start date of the time series.
        end: End date of the time series.
        granularity: Sampling frequency ('daily', 'weekly', 'monthly').
        points: Ordered list of valuation time series points.
    """

    investor_id: int | None = None
    family_id: int | None = None
    start: ConfiguredDate
    end: ConfiguredDate
    granularity: str
    points: list[ValueSeriesPoint] = Field(default_factory=list)

ValueSeriesPoint

Bases: FoliomanBaseModel

Single date point in net worth valuation series.

Attributes:

Name Type Description
date ConfiguredDate

Valuation point date.

value_inr ConfiguredDecimal

Total portfolio value in INR on this date.

invested_inr ConfiguredDecimal

Cumulative invested capital in INR on this date.

stale bool

Whether the valuation data for this date is stale.

Source code in src/folioman_client/models.py
class ValueSeriesPoint(FoliomanBaseModel):
    """Single date point in net worth valuation series.

    Attributes:
        date: Valuation point date.
        value_inr: Total portfolio value in INR on this date.
        invested_inr: Cumulative invested capital in INR on this date.
        stale: Whether the valuation data for this date is stale.
    """

    date: ConfiguredDate
    value_inr: ConfiguredDecimal
    invested_inr: ConfiguredDecimal
    stale: bool = False

options: show_root_heading: true members: - FoliomanClient - FoliomanSettings - settings


Client & Resources

folioman_client.client

Folioman API Client.

A thin, async, typed Python client for the Folioman REST API. Handles HTTP transport, JWT token lifecycle (obtain, store, inject, refresh, retry on 401), and response parsing into typed Pydantic models.

InvestorsResource

Bases: _BaseResource

Endpoints for managing and querying investors.

Source code in src/folioman_client/client.py
class InvestorsResource(_BaseResource):
    """Endpoints for managing and querying investors."""

    async def list(
        self,
        *,
        family_id: int | None = None,
        unaffiliated: bool = False,
    ) -> list[Investor]:
        """List investors accessible to the authenticated advisor.

        Args:
            family_id: Filter by parent family group ID.
            unaffiliated: If True, only returns investors not affiliated with any family.

        Returns:
            A list of Investor summary models.

        Raises:
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {}
        if family_id is not None:
            params["family_id"] = family_id
        if unaffiliated:
            params["unaffiliated"] = "true"

        data = await self._client.request("GET", "/investors/", params=params)
        return [Investor.model_validate(item) for item in data]

    async def get(self, investor_id: int) -> InvestorDetail:
        """Get investor details including masked PAN.

        Args:
            investor_id: Unique identifier of the investor.

        Returns:
            InvestorDetail model containing extended profile information.

        Raises:
            FoliomanNotFoundError: If the investor ID does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        data = await self._client.request("GET", f"/investors/{investor_id}")
        return InvestorDetail.model_validate(data)

list async

list(*, family_id: int | None = None, unaffiliated: bool = False) -> list[Investor]

List investors accessible to the authenticated advisor.

Parameters:

Name Type Description Default
family_id int | None

Filter by parent family group ID.

None
unaffiliated bool

If True, only returns investors not affiliated with any family.

False

Returns:

Type Description
list[Investor]

A list of Investor summary models.

Raises:

Type Description
FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    *,
    family_id: int | None = None,
    unaffiliated: bool = False,
) -> list[Investor]:
    """List investors accessible to the authenticated advisor.

    Args:
        family_id: Filter by parent family group ID.
        unaffiliated: If True, only returns investors not affiliated with any family.

    Returns:
        A list of Investor summary models.

    Raises:
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {}
    if family_id is not None:
        params["family_id"] = family_id
    if unaffiliated:
        params["unaffiliated"] = "true"

    data = await self._client.request("GET", "/investors/", params=params)
    return [Investor.model_validate(item) for item in data]

get async

get(investor_id: int) -> InvestorDetail

Get investor details including masked PAN.

Parameters:

Name Type Description Default
investor_id int

Unique identifier of the investor.

required

Returns:

Type Description
InvestorDetail

InvestorDetail model containing extended profile information.

Raises:

Type Description
FoliomanNotFoundError

If the investor ID does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(self, investor_id: int) -> InvestorDetail:
    """Get investor details including masked PAN.

    Args:
        investor_id: Unique identifier of the investor.

    Returns:
        InvestorDetail model containing extended profile information.

    Raises:
        FoliomanNotFoundError: If the investor ID does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    data = await self._client.request("GET", f"/investors/{investor_id}")
    return InvestorDetail.model_validate(data)

PortfolioResource

Bases: _BaseResource

Endpoints for investor portfolio summary and allocation.

Source code in src/folioman_client/client.py
class PortfolioResource(_BaseResource):
    """Endpoints for investor portfolio summary and allocation."""

    async def get(
        self,
        investor_id: int,
        *,
        as_of: date | str | None = None,
    ) -> PortfolioSummary:
        """Get the full portfolio summary, metrics, and asset mix for an investor.

        Args:
            investor_id: The ID of the investor.
            as_of: Optional point-in-time valuation date (YYYY-MM-DD or date object).

        Returns:
            PortfolioSummary model containing metrics, holdings, and allocation mixes.

        Raises:
            FoliomanNotFoundError: If the investor ID does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {}
        if as_of is not None:
            params["as_of"] = (
                as_of.isoformat() if isinstance(as_of, date) else str(as_of)
            )

        data = await self._client.request(
            "GET", f"/investors/{investor_id}/summary", params=params
        )
        return PortfolioSummary.model_validate(data)

get async

get(investor_id: int, *, as_of: date | str | None = None) -> PortfolioSummary

Get the full portfolio summary, metrics, and asset mix for an investor.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
as_of date | str | None

Optional point-in-time valuation date (YYYY-MM-DD or date object).

None

Returns:

Type Description
PortfolioSummary

PortfolioSummary model containing metrics, holdings, and allocation mixes.

Raises:

Type Description
FoliomanNotFoundError

If the investor ID does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(
    self,
    investor_id: int,
    *,
    as_of: date | str | None = None,
) -> PortfolioSummary:
    """Get the full portfolio summary, metrics, and asset mix for an investor.

    Args:
        investor_id: The ID of the investor.
        as_of: Optional point-in-time valuation date (YYYY-MM-DD or date object).

    Returns:
        PortfolioSummary model containing metrics, holdings, and allocation mixes.

    Raises:
        FoliomanNotFoundError: If the investor ID does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {}
    if as_of is not None:
        params["as_of"] = (
            as_of.isoformat() if isinstance(as_of, date) else str(as_of)
        )

    data = await self._client.request(
        "GET", f"/investors/{investor_id}/summary", params=params
    )
    return PortfolioSummary.model_validate(data)

HoldingsResource

Bases: _BaseResource

Endpoints for querying investor holdings and scheme details.

Source code in src/folioman_client/client.py
class HoldingsResource(_BaseResource):
    """Endpoints for querying investor holdings and scheme details."""

    async def list(
        self,
        investor_id: int,
        *,
        as_of: date | str | None = None,
    ) -> list[Holding]:
        """List priced holdings for an investor (extracted from portfolio summary).

        Args:
            investor_id: The ID of the investor.
            as_of: Optional point-in-time date.

        Returns:
            A list of Holding models.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        summary = await self._client.portfolio.get(investor_id, as_of=as_of)
        return summary.holdings

    async def get(
        self,
        investor_id: int,
        security_id: int,
        *,
        as_of: date | str | None = None,
    ) -> SchemeDetail:
        """Get detailed holding/scheme information (NAV history, transactions, folios).

        Args:
            investor_id: The ID of the investor.
            security_id: The ID of the security/scheme.
            as_of: Optional point-in-time valuation date.

        Returns:
            SchemeDetail model with full NAV points, folio balances, and transactions.

        Raises:
            FoliomanNotFoundError: If the investor or security does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {}
        if as_of is not None:
            params["as_of"] = (
                as_of.isoformat() if isinstance(as_of, date) else str(as_of)
            )

        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/holdings/{security_id}",
            params=params,
        )
        return SchemeDetail.model_validate(data)

list async

list(investor_id: int, *, as_of: date | str | None = None) -> list[Holding]

List priced holdings for an investor (extracted from portfolio summary).

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
as_of date | str | None

Optional point-in-time date.

None

Returns:

Type Description
list[Holding]

A list of Holding models.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    investor_id: int,
    *,
    as_of: date | str | None = None,
) -> list[Holding]:
    """List priced holdings for an investor (extracted from portfolio summary).

    Args:
        investor_id: The ID of the investor.
        as_of: Optional point-in-time date.

    Returns:
        A list of Holding models.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    summary = await self._client.portfolio.get(investor_id, as_of=as_of)
    return summary.holdings

get async

get(investor_id: int, security_id: int, *, as_of: date | str | None = None) -> SchemeDetail

Get detailed holding/scheme information (NAV history, transactions, folios).

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
security_id int

The ID of the security/scheme.

required
as_of date | str | None

Optional point-in-time valuation date.

None

Returns:

Type Description
SchemeDetail

SchemeDetail model with full NAV points, folio balances, and transactions.

Raises:

Type Description
FoliomanNotFoundError

If the investor or security does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(
    self,
    investor_id: int,
    security_id: int,
    *,
    as_of: date | str | None = None,
) -> SchemeDetail:
    """Get detailed holding/scheme information (NAV history, transactions, folios).

    Args:
        investor_id: The ID of the investor.
        security_id: The ID of the security/scheme.
        as_of: Optional point-in-time valuation date.

    Returns:
        SchemeDetail model with full NAV points, folio balances, and transactions.

    Raises:
        FoliomanNotFoundError: If the investor or security does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {}
    if as_of is not None:
        params["as_of"] = (
            as_of.isoformat() if isinstance(as_of, date) else str(as_of)
        )

    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/holdings/{security_id}",
        params=params,
    )
    return SchemeDetail.model_validate(data)

TransactionsResource

Bases: _BaseResource

Endpoints for querying transaction ledger entries.

Source code in src/folioman_client/client.py
class TransactionsResource(_BaseResource):
    """Endpoints for querying transaction ledger entries."""

    async def list(self, investor_id: int) -> list[Transaction]:
        """List all transactions for an investor.

        Args:
            investor_id: The ID of the investor.

        Returns:
            List of Transaction ledger entries.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        data = await self._client.request(
            "GET", f"/investors/{investor_id}/transactions"
        )
        return [Transaction.model_validate(item) for item in data]

list async

list(investor_id: int) -> list[Transaction]

List all transactions for an investor.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required

Returns:

Type Description
list[Transaction]

List of Transaction ledger entries.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(self, investor_id: int) -> list[Transaction]:
    """List all transactions for an investor.

    Args:
        investor_id: The ID of the investor.

    Returns:
        List of Transaction ledger entries.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    data = await self._client.request(
        "GET", f"/investors/{investor_id}/transactions"
    )
    return [Transaction.model_validate(item) for item in data]

ValuationsResource

Bases: _BaseResource

Endpoints for portfolio net-worth history and valuation status.

Source code in src/folioman_client/client.py
class ValuationsResource(_BaseResource):
    """Endpoints for portfolio net-worth history and valuation status."""

    async def list(
        self,
        investor_id: int,
        *,
        from_date: date | str | None = None,
        to_date: date | str | None = None,
        granularity: Literal["daily", "weekly", "monthly"] = "monthly",
    ) -> ValueSeries:
        """Get net-worth time series reconstructed from ledger and NAV history.

        Args:
            investor_id: The ID of the investor.
            from_date: Start date of series.
            to_date: End date of series.
            granularity: Sampling frequency ('daily', 'weekly', or 'monthly').

        Returns:
            ValueSeries model containing historical valuation points.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params: dict[str, Any] = {"granularity": granularity}
        if from_date is not None:
            params["from"] = (
                from_date.isoformat() if isinstance(from_date, date) else str(from_date)
            )
        if to_date is not None:
            params["to"] = (
                to_date.isoformat() if isinstance(to_date, date) else str(to_date)
            )

        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/value-series",
            params=params,
        )
        return ValueSeries.model_validate(data)

    async def status(self, investor_id: int) -> ValuationStatus:
        """Get the current valuation calculation readiness status for an investor.

        Args:
            investor_id: The ID of the investor.

        Returns:
            ValuationStatus model indicating calculation state and coverage.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        data = await self._client.request(
            "GET", f"/investors/{investor_id}/valuation-status"
        )
        return ValuationStatus.model_validate(data)

list async

list(investor_id: int, *, from_date: date | str | None = None, to_date: date | str | None = None, granularity: Literal['daily', 'weekly', 'monthly'] = 'monthly') -> ValueSeries

Get net-worth time series reconstructed from ledger and NAV history.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
from_date date | str | None

Start date of series.

None
to_date date | str | None

End date of series.

None
granularity Literal['daily', 'weekly', 'monthly']

Sampling frequency ('daily', 'weekly', or 'monthly').

'monthly'

Returns:

Type Description
ValueSeries

ValueSeries model containing historical valuation points.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    investor_id: int,
    *,
    from_date: date | str | None = None,
    to_date: date | str | None = None,
    granularity: Literal["daily", "weekly", "monthly"] = "monthly",
) -> ValueSeries:
    """Get net-worth time series reconstructed from ledger and NAV history.

    Args:
        investor_id: The ID of the investor.
        from_date: Start date of series.
        to_date: End date of series.
        granularity: Sampling frequency ('daily', 'weekly', or 'monthly').

    Returns:
        ValueSeries model containing historical valuation points.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params: dict[str, Any] = {"granularity": granularity}
    if from_date is not None:
        params["from"] = (
            from_date.isoformat() if isinstance(from_date, date) else str(from_date)
        )
    if to_date is not None:
        params["to"] = (
            to_date.isoformat() if isinstance(to_date, date) else str(to_date)
        )

    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/value-series",
        params=params,
    )
    return ValueSeries.model_validate(data)

status async

status(investor_id: int) -> ValuationStatus

Get the current valuation calculation readiness status for an investor.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required

Returns:

Type Description
ValuationStatus

ValuationStatus model indicating calculation state and coverage.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def status(self, investor_id: int) -> ValuationStatus:
    """Get the current valuation calculation readiness status for an investor.

    Args:
        investor_id: The ID of the investor.

    Returns:
        ValuationStatus model indicating calculation state and coverage.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    data = await self._client.request(
        "GET", f"/investors/{investor_id}/valuation-status"
    )
    return ValuationStatus.model_validate(data)

CapitalGainsResource

Bases: _BaseResource

Endpoints for realised capital gains reporting.

Source code in src/folioman_client/client.py
class CapitalGainsResource(_BaseResource):
    """Endpoints for realised capital gains reporting."""

    async def list(
        self,
        investor_id: int,
        *,
        include_unreconciled: bool = False,
    ) -> list[CapitalGainsFyPoint]:
        """List realised STCG/LTCG across every financial year with disposals.

        Args:
            investor_id: The ID of the investor.
            include_unreconciled: Whether to include unreconciled transactions.

        Returns:
            List of CapitalGainsFyPoint models summarizing capital gains by financial year.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params = {"include_unreconciled": str(include_unreconciled).lower()}
        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/reports/capital-gains-by-fy",
            params=params,
        )
        return [CapitalGainsFyPoint.model_validate(item) for item in data]

    async def get(
        self,
        investor_id: int,
        *,
        fy: str,
        include_unreconciled: bool = False,
    ) -> CapitalGainsReport:
        """Get realised capital gains report for a specific financial year.

        Args:
            investor_id: The ID of the investor.
            fy: Financial year string, e.g. "2024-25".
            include_unreconciled: Whether to include unreconciled transactions.

        Returns:
            CapitalGainsReport model with detailed lot-level gains.

        Raises:
            FoliomanNotFoundError: If the investor does not exist.
            FoliomanAuthError: If authentication fails.
            FoliomanAPIError: If the server returns an error response.
        """
        params = {
            "fy": fy,
            "include_unreconciled": str(include_unreconciled).lower(),
        }
        data = await self._client.request(
            "GET",
            f"/investors/{investor_id}/exports/capital-gains",
            params=params,
        )
        return CapitalGainsReport.model_validate(data)

list async

list(investor_id: int, *, include_unreconciled: bool = False) -> list[CapitalGainsFyPoint]

List realised STCG/LTCG across every financial year with disposals.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
include_unreconciled bool

Whether to include unreconciled transactions.

False

Returns:

Type Description
list[CapitalGainsFyPoint]

List of CapitalGainsFyPoint models summarizing capital gains by financial year.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def list(
    self,
    investor_id: int,
    *,
    include_unreconciled: bool = False,
) -> list[CapitalGainsFyPoint]:
    """List realised STCG/LTCG across every financial year with disposals.

    Args:
        investor_id: The ID of the investor.
        include_unreconciled: Whether to include unreconciled transactions.

    Returns:
        List of CapitalGainsFyPoint models summarizing capital gains by financial year.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params = {"include_unreconciled": str(include_unreconciled).lower()}
    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/reports/capital-gains-by-fy",
        params=params,
    )
    return [CapitalGainsFyPoint.model_validate(item) for item in data]

get async

get(investor_id: int, *, fy: str, include_unreconciled: bool = False) -> CapitalGainsReport

Get realised capital gains report for a specific financial year.

Parameters:

Name Type Description Default
investor_id int

The ID of the investor.

required
fy str

Financial year string, e.g. "2024-25".

required
include_unreconciled bool

Whether to include unreconciled transactions.

False

Returns:

Type Description
CapitalGainsReport

CapitalGainsReport model with detailed lot-level gains.

Raises:

Type Description
FoliomanNotFoundError

If the investor does not exist.

FoliomanAuthError

If authentication fails.

FoliomanAPIError

If the server returns an error response.

Source code in src/folioman_client/client.py
async def get(
    self,
    investor_id: int,
    *,
    fy: str,
    include_unreconciled: bool = False,
) -> CapitalGainsReport:
    """Get realised capital gains report for a specific financial year.

    Args:
        investor_id: The ID of the investor.
        fy: Financial year string, e.g. "2024-25".
        include_unreconciled: Whether to include unreconciled transactions.

    Returns:
        CapitalGainsReport model with detailed lot-level gains.

    Raises:
        FoliomanNotFoundError: If the investor does not exist.
        FoliomanAuthError: If authentication fails.
        FoliomanAPIError: If the server returns an error response.
    """
    params = {
        "fy": fy,
        "include_unreconciled": str(include_unreconciled).lower(),
    }
    data = await self._client.request(
        "GET",
        f"/investors/{investor_id}/exports/capital-gains",
        params=params,
    )
    return CapitalGainsReport.model_validate(data)

FoliomanClient

Asynchronous client for interacting with the Folioman API.

Handles authentication, token refresh, and request execution.

Example
import asyncio
from folioman_client import FoliomanClient

async def main() -> None:
    async with FoliomanClient() as client:
        investor = await client.investors.get(1)
        summary = await client.portfolio.get(1)
        print(investor.name, summary.total_inr)

asyncio.run(main())
Source code in src/folioman_client/client.py
class FoliomanClient:
    """Asynchronous client for interacting with the Folioman API.

    Handles authentication, token refresh, and request execution.

    Example:
        ```python
        import asyncio
        from folioman_client import FoliomanClient

        async def main() -> None:
            async with FoliomanClient() as client:
                investor = await client.investors.get(1)
                summary = await client.portfolio.get(1)
                print(investor.name, summary.total_inr)

        asyncio.run(main())
        ```
    """

    def __init__(
        self,
        base_url: str | None = None,
        username: str | None = None,
        password: str | None = None,
        timeout: float = 30.0,
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        """Initialize FoliomanClient.

        Args:
            base_url: Base URL of the Folioman REST API. If None, loaded from settings/environment.
            username: Username for API authentication. If None, loaded from settings/environment.
            password: Password for API authentication. If None, loaded from settings/environment.
            timeout: Request timeout in seconds. Defaults to 30.0.
            http_client: Optional custom httpx.AsyncClient instance for custom transport/pooling.
        """
        self.base_url = (base_url or default_settings.base_url).rstrip("/")
        self.username = username if username is not None else default_settings.username
        self.password = password if password is not None else default_settings.password
        self.timeout = timeout

        self._auth = JWTAuthManager(
            base_url=self.base_url,
            username=self.username,
            password=self.password,
        )

        self._owns_http_client = http_client is None
        self._http_client = http_client or httpx.AsyncClient(
            base_url=self.base_url,
            timeout=self.timeout,
        )

        # Resource sub-clients
        self.investors = InvestorsResource(self)
        self.portfolio = PortfolioResource(self)
        self.holdings = HoldingsResource(self)
        self.transactions = TransactionsResource(self)
        self.valuations = ValuationsResource(self)
        self.capital_gains = CapitalGainsResource(self)

    @classmethod
    def from_settings(cls, settings: FoliomanSettings) -> FoliomanClient:
        """Create a client instance from a FoliomanSettings object.

        Args:
            settings: FoliomanSettings configuration object.

        Returns:
            A configured FoliomanClient instance.
        """
        return cls(
            base_url=settings.base_url,
            username=settings.username,
            password=settings.password,
            timeout=settings.timeout,
        )

    @classmethod
    def from_env(cls) -> FoliomanClient:
        """Create a client instance using environment variables.

        Returns:
            A configured FoliomanClient instance using environment defaults.
        """
        return cls.from_settings(FoliomanSettings())

    async def __aenter__(self) -> FoliomanClient:
        """Enter the async context manager.

        Returns:
            The FoliomanClient instance.
        """
        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Exit the async context manager and close HTTP connections."""
        await self.close()

    async def close(self) -> None:
        """Close the underlying HTTP transport if owned by this client."""
        if self._owns_http_client:
            await self._http_client.aclose()

    def _normalize_path(self, path: str) -> str:
        """Ensure path starts with /api prefix as required by Folioman API.

        Args:
            path: Relative API path string.

        Returns:
            Normalized path starting with '/api/'.
        """
        path = path.strip()
        if not path.startswith("/"):
            path = f"/{path}"
        if not path.startswith("/api/"):
            path = f"/api{path}"
        return path

    async def request(
        self,
        method: str,
        path: str,
        *,
        params: dict[str, Any] | None = None,
        json: Any | None = None,
        headers: dict[str, str] | None = None,
        **kwargs: Any,
    ) -> Any:
        """Execute an authenticated HTTP request with automatic token refresh and 401 retry.

        Args:
            method: HTTP method ("GET", "POST", etc.)
            path: Relative API path (e.g. "/investors/1")
            params: Optional query parameters.
            json: Optional JSON request payload.
            headers: Optional extra headers.
            **kwargs: Additional keyword arguments passed to httpx.AsyncClient.request.

        Returns:
            Parsed JSON response, or None if status code is 204 or body is empty.

        Raises:
            FoliomanNotFoundError: If response is 404.
            FoliomanAuthError: If authentication fails or refresh is rejected.
            FoliomanAPIError: If server responds with other 4xx or 5xx status codes.
        """
        normalized_path = self._normalize_path(path)
        req_headers = dict(headers or {})

        # Obtain valid Bearer token
        token = await self._auth.get_valid_token(self._http_client)
        req_headers["Authorization"] = f"Bearer {token}"

        response = await self._http_client.request(
            method=method,
            url=normalized_path,
            params=params,
            json=json,
            headers=req_headers,
            **kwargs,
        )

        # Reactive refresh if 401 Unauthorized occurs
        if response.status_code == 401:
            new_token = await self._auth.force_refresh(self._http_client)
            req_headers["Authorization"] = f"Bearer {new_token}"
            response = await self._http_client.request(
                method=method,
                url=normalized_path,
                params=params,
                json=json,
                headers=req_headers,
                **kwargs,
            )

        return self._handle_response(response, normalized_path)

    def _handle_response(self, response: httpx.Response, path: str) -> Any:
        """Process HTTP response, translating errors to Folioman client exceptions.

        Args:
            response: httpx.Response object.
            path: Normalized path requested.

        Returns:
            Parsed response JSON or None.

        Raises:
            FoliomanNotFoundError: If response status is 404.
            FoliomanAuthError: If response status is 401.
            FoliomanAPIError: If response status is not successful or parsing fails.
        """
        if response.status_code == 404:
            raise FoliomanNotFoundError(f"Resource not found at {path}")

        if response.status_code == 401:
            raise FoliomanAuthError(f"Authentication failed for {path} (HTTP 401)")

        if not response.is_success:
            detail: Any = None
            try:
                data = response.json()
                detail = data.get("detail", data)
            except Exception:
                detail = response.text
            raise FoliomanAPIError(
                message=f"Folioman API error on {response.request.method} {path}: {detail}",
                status_code=response.status_code,
                response_data=detail,
            )

        if response.status_code == 204 or not response.content:
            return None

        try:
            return response.json()
        except Exception as exc:
            raise FoliomanAPIError(
                message=f"Failed to parse JSON response from {path}: {exc}",
                status_code=response.status_code,
                response_data=response.text,
            ) from exc

__init__

__init__(base_url: str | None = None, username: str | None = None, password: str | None = None, timeout: float = 30.0, http_client: AsyncClient | None = None) -> None

Initialize FoliomanClient.

Parameters:

Name Type Description Default
base_url str | None

Base URL of the Folioman REST API. If None, loaded from settings/environment.

None
username str | None

Username for API authentication. If None, loaded from settings/environment.

None
password str | None

Password for API authentication. If None, loaded from settings/environment.

None
timeout float

Request timeout in seconds. Defaults to 30.0.

30.0
http_client AsyncClient | None

Optional custom httpx.AsyncClient instance for custom transport/pooling.

None
Source code in src/folioman_client/client.py
def __init__(
    self,
    base_url: str | None = None,
    username: str | None = None,
    password: str | None = None,
    timeout: float = 30.0,
    http_client: httpx.AsyncClient | None = None,
) -> None:
    """Initialize FoliomanClient.

    Args:
        base_url: Base URL of the Folioman REST API. If None, loaded from settings/environment.
        username: Username for API authentication. If None, loaded from settings/environment.
        password: Password for API authentication. If None, loaded from settings/environment.
        timeout: Request timeout in seconds. Defaults to 30.0.
        http_client: Optional custom httpx.AsyncClient instance for custom transport/pooling.
    """
    self.base_url = (base_url or default_settings.base_url).rstrip("/")
    self.username = username if username is not None else default_settings.username
    self.password = password if password is not None else default_settings.password
    self.timeout = timeout

    self._auth = JWTAuthManager(
        base_url=self.base_url,
        username=self.username,
        password=self.password,
    )

    self._owns_http_client = http_client is None
    self._http_client = http_client or httpx.AsyncClient(
        base_url=self.base_url,
        timeout=self.timeout,
    )

    # Resource sub-clients
    self.investors = InvestorsResource(self)
    self.portfolio = PortfolioResource(self)
    self.holdings = HoldingsResource(self)
    self.transactions = TransactionsResource(self)
    self.valuations = ValuationsResource(self)
    self.capital_gains = CapitalGainsResource(self)

from_settings classmethod

from_settings(settings: FoliomanSettings) -> FoliomanClient

Create a client instance from a FoliomanSettings object.

Parameters:

Name Type Description Default
settings FoliomanSettings

FoliomanSettings configuration object.

required

Returns:

Type Description
FoliomanClient

A configured FoliomanClient instance.

Source code in src/folioman_client/client.py
@classmethod
def from_settings(cls, settings: FoliomanSettings) -> FoliomanClient:
    """Create a client instance from a FoliomanSettings object.

    Args:
        settings: FoliomanSettings configuration object.

    Returns:
        A configured FoliomanClient instance.
    """
    return cls(
        base_url=settings.base_url,
        username=settings.username,
        password=settings.password,
        timeout=settings.timeout,
    )

from_env classmethod

from_env() -> FoliomanClient

Create a client instance using environment variables.

Returns:

Type Description
FoliomanClient

A configured FoliomanClient instance using environment defaults.

Source code in src/folioman_client/client.py
@classmethod
def from_env(cls) -> FoliomanClient:
    """Create a client instance using environment variables.

    Returns:
        A configured FoliomanClient instance using environment defaults.
    """
    return cls.from_settings(FoliomanSettings())

__aenter__ async

__aenter__() -> FoliomanClient

Enter the async context manager.

Returns:

Type Description
FoliomanClient

The FoliomanClient instance.

Source code in src/folioman_client/client.py
async def __aenter__(self) -> FoliomanClient:
    """Enter the async context manager.

    Returns:
        The FoliomanClient instance.
    """
    return self

__aexit__ async

__aexit__(exc_type: Any, exc_val: Any, exc_tb: Any) -> None

Exit the async context manager and close HTTP connections.

Source code in src/folioman_client/client.py
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    """Exit the async context manager and close HTTP connections."""
    await self.close()

close async

close() -> None

Close the underlying HTTP transport if owned by this client.

Source code in src/folioman_client/client.py
async def close(self) -> None:
    """Close the underlying HTTP transport if owned by this client."""
    if self._owns_http_client:
        await self._http_client.aclose()

request async

request(method: str, path: str, *, params: dict[str, Any] | None = None, json: Any | None = None, headers: dict[str, str] | None = None, **kwargs: Any) -> Any

Execute an authenticated HTTP request with automatic token refresh and 401 retry.

Parameters:

Name Type Description Default
method str

HTTP method ("GET", "POST", etc.)

required
path str

Relative API path (e.g. "/investors/1")

required
params dict[str, Any] | None

Optional query parameters.

None
json Any | None

Optional JSON request payload.

None
headers dict[str, str] | None

Optional extra headers.

None
**kwargs Any

Additional keyword arguments passed to httpx.AsyncClient.request.

{}

Returns:

Type Description
Any

Parsed JSON response, or None if status code is 204 or body is empty.

Raises:

Type Description
FoliomanNotFoundError

If response is 404.

FoliomanAuthError

If authentication fails or refresh is rejected.

FoliomanAPIError

If server responds with other 4xx or 5xx status codes.

Source code in src/folioman_client/client.py
async def request(
    self,
    method: str,
    path: str,
    *,
    params: dict[str, Any] | None = None,
    json: Any | None = None,
    headers: dict[str, str] | None = None,
    **kwargs: Any,
) -> Any:
    """Execute an authenticated HTTP request with automatic token refresh and 401 retry.

    Args:
        method: HTTP method ("GET", "POST", etc.)
        path: Relative API path (e.g. "/investors/1")
        params: Optional query parameters.
        json: Optional JSON request payload.
        headers: Optional extra headers.
        **kwargs: Additional keyword arguments passed to httpx.AsyncClient.request.

    Returns:
        Parsed JSON response, or None if status code is 204 or body is empty.

    Raises:
        FoliomanNotFoundError: If response is 404.
        FoliomanAuthError: If authentication fails or refresh is rejected.
        FoliomanAPIError: If server responds with other 4xx or 5xx status codes.
    """
    normalized_path = self._normalize_path(path)
    req_headers = dict(headers or {})

    # Obtain valid Bearer token
    token = await self._auth.get_valid_token(self._http_client)
    req_headers["Authorization"] = f"Bearer {token}"

    response = await self._http_client.request(
        method=method,
        url=normalized_path,
        params=params,
        json=json,
        headers=req_headers,
        **kwargs,
    )

    # Reactive refresh if 401 Unauthorized occurs
    if response.status_code == 401:
        new_token = await self._auth.force_refresh(self._http_client)
        req_headers["Authorization"] = f"Bearer {new_token}"
        response = await self._http_client.request(
            method=method,
            url=normalized_path,
            params=params,
            json=json,
            headers=req_headers,
            **kwargs,
        )

    return self._handle_response(response, normalized_path)

options: show_root_heading: true


Authentication

folioman_client.auth

JWT Authentication Manager for Folioman.

Handles initial token generation, automatic refresh, proactive expiry detection, and concurrency-safe token renewal.

JWTAuthManager

Manages JWT authentication state, token storage, and refresh lifecycle.

Keeps tokens private so they are never leaked outside the client.

Source code in src/folioman_client/auth.py
class JWTAuthManager:
    """Manages JWT authentication state, token storage, and refresh lifecycle.

    Keeps tokens private so they are never leaked outside the client.
    """

    def __init__(
        self,
        base_url: str,
        username: str,
        password: str,
    ) -> None:
        """Initialize JWTAuthManager with API credentials.

        Args:
            base_url: Base URL of the Folioman server.
            username: Username for authentication.
            password: Password for authentication.
        """
        self._base_url = base_url.rstrip("/")
        self._username = username
        self._password = password
        self._access_token: str | None = None
        self._refresh_token: str | None = None
        self._lock = asyncio.Lock()

    @property
    def has_tokens(self) -> bool:
        """True if the manager currently holds an access or refresh token."""
        return self._access_token is not None or self._refresh_token is not None

    def clear(self) -> None:
        """Clear cached access and refresh tokens."""
        self._access_token = None
        self._refresh_token = None

    async def get_valid_token(self, client: httpx.AsyncClient) -> str:
        """Return a valid access token, proactively refreshing or authenticating if needed.

        Args:
            client: The httpx AsyncClient transport instance to execute requests.

        Returns:
            A valid JWT access token string.

        Raises:
            FoliomanAuthError: If authentication or refresh fails.
        """
        # Fast path outside the lock if the current access token is fresh
        if self._access_token and not _is_expired(self._access_token):
            return self._access_token

        async with self._lock:
            # Re-check under lock in case another coroutine refreshed it
            if self._access_token and not _is_expired(self._access_token):
                return self._access_token

            # Try refresh if we have a refresh token
            if self._refresh_token:
                try:
                    return await self._refresh_access_token(client)
                except Exception:
                    # If refresh fails, fall back to initial authentication
                    pass

            # Otherwise, authenticate from credentials
            return await self._authenticate(client)

    async def force_refresh(self, client: httpx.AsyncClient) -> str:
        """Force a refresh or re-authentication after receiving a 401.

        Args:
            client: The httpx AsyncClient transport instance to execute requests.

        Returns:
            A new valid JWT access token string.

        Raises:
            FoliomanAuthError: If renewal or re-authentication fails.
        """
        async with self._lock:
            if self._refresh_token:
                try:
                    return await self._refresh_access_token(client)
                except Exception:
                    pass

            return await self._authenticate(client)

    async def _authenticate(self, client: httpx.AsyncClient) -> str:
        """Authenticate with username and password (/api/auth/token/pair).

        Args:
            client: The httpx AsyncClient transport instance.

        Returns:
            A newly acquired access token string.

        Raises:
            FoliomanAuthError: If authentication fails or response is invalid.
        """
        url = f"{self._base_url}/api/auth/token/pair"
        payload = {"username": self._username, "password": self._password}

        try:
            response = await client.post(url, json=payload)
        except Exception as exc:
            raise FoliomanAuthError(
                f"Network error during authentication: {exc}"
            ) from exc

        if response.status_code == 401:
            raise FoliomanAuthError("Invalid username or password.")
        if not response.is_success:
            raise FoliomanAuthError(
                f"Authentication failed with status {response.status_code}: {response.text}"
            )

        data = response.json()
        access = data.get("access")
        refresh = data.get("refresh")
        if not access or not refresh:
            raise FoliomanAuthError(
                "Malformed authentication response: missing tokens."
            )

        self._access_token = access
        self._refresh_token = refresh
        return access

    async def _refresh_access_token(self, client: httpx.AsyncClient) -> str:
        """Mint a fresh access token from the refresh token (/api/auth/token/refresh).

        Args:
            client: The httpx AsyncClient transport instance.

        Returns:
            A refreshed access token string.

        Raises:
            FoliomanAuthError: If refresh token is expired, invalid, or missing.
        """
        if not self._refresh_token:
            raise FoliomanAuthError("No refresh token available.")

        url = f"{self._base_url}/api/auth/token/refresh"
        payload = {"refresh": self._refresh_token}

        try:
            response = await client.post(url, json=payload)
        except Exception as exc:
            raise FoliomanAuthError(
                f"Network error during token refresh: {exc}"
            ) from exc

        if response.status_code == 401:
            self.clear()
            raise FoliomanAuthError("Refresh token expired or invalid.")
        if not response.is_success:
            self.clear()
            raise FoliomanAuthError(
                f"Token refresh failed with status {response.status_code}: {response.text}"
            )

        data = response.json()
        access = data.get("access")
        if not access:
            self.clear()
            raise FoliomanAuthError("Malformed refresh response: missing access token.")

        self._access_token = access
        return access

has_tokens property

has_tokens: bool

True if the manager currently holds an access or refresh token.

__init__

__init__(base_url: str, username: str, password: str) -> None

Initialize JWTAuthManager with API credentials.

Parameters:

Name Type Description Default
base_url str

Base URL of the Folioman server.

required
username str

Username for authentication.

required
password str

Password for authentication.

required
Source code in src/folioman_client/auth.py
def __init__(
    self,
    base_url: str,
    username: str,
    password: str,
) -> None:
    """Initialize JWTAuthManager with API credentials.

    Args:
        base_url: Base URL of the Folioman server.
        username: Username for authentication.
        password: Password for authentication.
    """
    self._base_url = base_url.rstrip("/")
    self._username = username
    self._password = password
    self._access_token: str | None = None
    self._refresh_token: str | None = None
    self._lock = asyncio.Lock()

clear

clear() -> None

Clear cached access and refresh tokens.

Source code in src/folioman_client/auth.py
def clear(self) -> None:
    """Clear cached access and refresh tokens."""
    self._access_token = None
    self._refresh_token = None

get_valid_token async

get_valid_token(client: AsyncClient) -> str

Return a valid access token, proactively refreshing or authenticating if needed.

Parameters:

Name Type Description Default
client AsyncClient

The httpx AsyncClient transport instance to execute requests.

required

Returns:

Type Description
str

A valid JWT access token string.

Raises:

Type Description
FoliomanAuthError

If authentication or refresh fails.

Source code in src/folioman_client/auth.py
async def get_valid_token(self, client: httpx.AsyncClient) -> str:
    """Return a valid access token, proactively refreshing or authenticating if needed.

    Args:
        client: The httpx AsyncClient transport instance to execute requests.

    Returns:
        A valid JWT access token string.

    Raises:
        FoliomanAuthError: If authentication or refresh fails.
    """
    # Fast path outside the lock if the current access token is fresh
    if self._access_token and not _is_expired(self._access_token):
        return self._access_token

    async with self._lock:
        # Re-check under lock in case another coroutine refreshed it
        if self._access_token and not _is_expired(self._access_token):
            return self._access_token

        # Try refresh if we have a refresh token
        if self._refresh_token:
            try:
                return await self._refresh_access_token(client)
            except Exception:
                # If refresh fails, fall back to initial authentication
                pass

        # Otherwise, authenticate from credentials
        return await self._authenticate(client)

force_refresh async

force_refresh(client: AsyncClient) -> str

Force a refresh or re-authentication after receiving a 401.

Parameters:

Name Type Description Default
client AsyncClient

The httpx AsyncClient transport instance to execute requests.

required

Returns:

Type Description
str

A new valid JWT access token string.

Raises:

Type Description
FoliomanAuthError

If renewal or re-authentication fails.

Source code in src/folioman_client/auth.py
async def force_refresh(self, client: httpx.AsyncClient) -> str:
    """Force a refresh or re-authentication after receiving a 401.

    Args:
        client: The httpx AsyncClient transport instance to execute requests.

    Returns:
        A new valid JWT access token string.

    Raises:
        FoliomanAuthError: If renewal or re-authentication fails.
    """
    async with self._lock:
        if self._refresh_token:
            try:
                return await self._refresh_access_token(client)
            except Exception:
                pass

        return await self._authenticate(client)

options: show_root_heading: true


Configuration

folioman_client.config

Configuration settings for Folioman Client.

FoliomanSettings

Bases: BaseSettings

Folioman client configuration backed by environment variables.

Attributes:

Name Type Description
base_url str

The base URL of the Folioman REST API service. Defaults to "http://localhost:8000".

username str

The username used for HTTP basic or JWT token retrieval. Defaults to "".

password str

The password used for HTTP basic or JWT token retrieval. Defaults to "".

timeout float

The request timeout in seconds. Defaults to 30.0.

Source code in src/folioman_client/config.py
class FoliomanSettings(BaseSettings):
    """Folioman client configuration backed by environment variables.

    Attributes:
        base_url: The base URL of the Folioman REST API service.
            Defaults to "http://localhost:8000".
        username: The username used for HTTP basic or JWT token retrieval.
            Defaults to "".
        password: The password used for HTTP basic or JWT token retrieval.
            Defaults to "".
        timeout: The request timeout in seconds.
            Defaults to 30.0.
    """

    model_config = SettingsConfigDict(
        env_prefix="FOLIOMAN_",
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    base_url: str = Field(
        default="http://localhost:8000",
        description="Base URL of the Folioman REST API service.",
    )
    username: str = Field(
        default="",
        description="Username or advisor identifier for authentication.",
    )
    password: str = Field(
        default="",
        description="Password or secret credential for authentication.",
    )
    timeout: float = Field(
        default=30.0,
        description="HTTP request timeout in seconds.",
    )

    @property
    def folioman_url(self) -> str:
        """Alias for base_url."""
        return self.base_url

    @property
    def folioman_username(self) -> str:
        """Alias for username."""
        return self.username

    @property
    def folioman_password(self) -> str:
        """Alias for password."""
        return self.password

folioman_url property

folioman_url: str

Alias for base_url.

folioman_username property

folioman_username: str

Alias for username.

folioman_password property

folioman_password: str

Alias for password.

options: show_root_heading: true


Models

folioman_client.models

Pydantic models representing Folioman API schemas.

These models mirror the OpenAPI contracts in Folioman (v1). All models use extra="ignore" to remain resilient against future schema extensions.

ConfiguredDate module-attribute

ConfiguredDate = Annotated[date, PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used='unless-none')]

Date type serialized to ISO-8601 string (YYYY-MM-DD) unless None.

ConfiguredDatetime module-attribute

ConfiguredDatetime = Annotated[datetime, PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used='unless-none')]

Datetime type serialized to ISO-8601 string unless None.

ConfiguredDecimal module-attribute

ConfiguredDecimal = Annotated[Decimal, PlainSerializer(lambda x: float(x), return_type=float, when_used='unless-none')]

Decimal type serialized to float unless None.

FoliomanBaseModel

Bases: BaseModel

Base model with common configuration for all Folioman models.

Ignores extra keys sent by the API for forward compatibility and supports field population by name.

Source code in src/folioman_client/models.py
class FoliomanBaseModel(BaseModel):
    """Base model with common configuration for all Folioman models.

    Ignores extra keys sent by the API for forward compatibility and
    supports field population by name.
    """

    model_config = ConfigDict(
        extra="ignore",
        populate_by_name=True,
    )

TokenPair

Bases: FoliomanBaseModel

Access and refresh token pair returned on authentication.

Attributes:

Name Type Description
access str

Short-lived JWT bearer token used for authorizing requests.

refresh str

Long-lived refresh token used to obtain renewed access tokens.

Source code in src/folioman_client/models.py
class TokenPair(FoliomanBaseModel):
    """Access and refresh token pair returned on authentication.

    Attributes:
        access: Short-lived JWT bearer token used for authorizing requests.
        refresh: Long-lived refresh token used to obtain renewed access tokens.
    """

    access: str
    refresh: str

AccessToken

Bases: FoliomanBaseModel

Refreshed access token.

Attributes:

Name Type Description
access str

Newly minted short-lived JWT bearer token.

Source code in src/folioman_client/models.py
class AccessToken(FoliomanBaseModel):
    """Refreshed access token.

    Attributes:
        access: Newly minted short-lived JWT bearer token.
    """

    access: str

Investor

Bases: FoliomanBaseModel

Investor summary representation.

Attributes:

Name Type Description
id int

Unique numeric identifier of the investor.

name str

Full legal name or display name of the investor.

email str

Contact email address of the investor.

is_huf bool

Whether the investor represents a Hindu Undivided Family.

relation str

Relationship description if part of a family group.

family_id int | None

ID of the parent family group, if affiliated.

has_pan bool

Whether a Permanent Account Number is registered.

pan_locked bool

Whether PAN changes are locked for compliance.

created_at ConfiguredDatetime | None

Timestamp when the investor record was created.

updated_at ConfiguredDatetime | None

Timestamp when the investor record was last updated.

Source code in src/folioman_client/models.py
class Investor(FoliomanBaseModel):
    """Investor summary representation.

    Attributes:
        id: Unique numeric identifier of the investor.
        name: Full legal name or display name of the investor.
        email: Contact email address of the investor.
        is_huf: Whether the investor represents a Hindu Undivided Family.
        relation: Relationship description if part of a family group.
        family_id: ID of the parent family group, if affiliated.
        has_pan: Whether a Permanent Account Number is registered.
        pan_locked: Whether PAN changes are locked for compliance.
        created_at: Timestamp when the investor record was created.
        updated_at: Timestamp when the investor record was last updated.
    """

    id: int
    name: str
    email: str = ""
    is_huf: bool = False
    relation: str = ""
    family_id: int | None = None
    has_pan: bool = False
    pan_locked: bool = False
    created_at: ConfiguredDatetime | None = None
    updated_at: ConfiguredDatetime | None = None

InvestorDetail

Bases: Investor

Investor detailed representation with masked PAN.

Attributes:

Name Type Description
pan_masked str

Masked PAN string (e.g. 'ABCDE****F') protecting sensitive PII.

Source code in src/folioman_client/models.py
class InvestorDetail(Investor):
    """Investor detailed representation with masked PAN.

    Attributes:
        pan_masked: Masked PAN string (e.g. 'ABCDE****F') protecting sensitive PII.
    """

    pan_masked: str = ""

Holding

Bases: FoliomanBaseModel

Priced holding row under an investor.

Attributes:

Name Type Description
security_id int

Unique identifier for the underlying security/scheme.

name str

Name of the security or mutual fund scheme.

security_type str

Category of the security (e.g., 'MF', 'EQUITY').

symbol str

Ticker symbol or trading identifier.

amc str

Asset Management Company name.

category str

SEBI category or mutual fund classification.

units ConfiguredDecimal

Total quantity of units held.

value_inr ConfiguredDecimal | None

Current market valuation in INR.

invested_inr ConfiguredDecimal | None

Total invested amount (cost basis) in INR.

latest_nav ConfiguredDecimal | None

Latest available Net Asset Value.

return_pct float | None

Absolute percentage return.

xirr float | None

Extended Internal Rate of Return (annualized).

day_change_inr ConfiguredDecimal | None

Monetary change in valuation since the previous trading day.

day_change_pct float | None

Percentage change since the previous trading day.

Source code in src/folioman_client/models.py
class Holding(FoliomanBaseModel):
    """Priced holding row under an investor.

    Attributes:
        security_id: Unique identifier for the underlying security/scheme.
        name: Name of the security or mutual fund scheme.
        security_type: Category of the security (e.g., 'MF', 'EQUITY').
        symbol: Ticker symbol or trading identifier.
        amc: Asset Management Company name.
        category: SEBI category or mutual fund classification.
        units: Total quantity of units held.
        value_inr: Current market valuation in INR.
        invested_inr: Total invested amount (cost basis) in INR.
        latest_nav: Latest available Net Asset Value.
        return_pct: Absolute percentage return.
        xirr: Extended Internal Rate of Return (annualized).
        day_change_inr: Monetary change in valuation since the previous trading day.
        day_change_pct: Percentage change since the previous trading day.
    """

    security_id: int
    name: str
    security_type: str
    symbol: str = ""
    amc: str = ""
    category: str = ""
    units: ConfiguredDecimal
    value_inr: ConfiguredDecimal | None = None
    invested_inr: ConfiguredDecimal | None = None
    latest_nav: ConfiguredDecimal | None = None
    return_pct: float | None = None
    xirr: float | None = None
    day_change_inr: ConfiguredDecimal | None = None
    day_change_pct: float | None = None

SchemeRef

Bases: FoliomanBaseModel

Security identity metadata.

Attributes:

Name Type Description
id int

Unique identifier of the security.

name str

Full name of the mutual fund scheme or security.

isin str

International Securities Identification Number.

symbol str

Ticker symbol if traded on an exchange.

security_type str

Type of security (e.g. 'MF', 'EQUITY').

amfi_code str

Association of Mutual Funds in India identifier.

amc str | None

Asset Management Company managing the scheme.

category str | None

Scheme investment category.

Source code in src/folioman_client/models.py
class SchemeRef(FoliomanBaseModel):
    """Security identity metadata.

    Attributes:
        id: Unique identifier of the security.
        name: Full name of the mutual fund scheme or security.
        isin: International Securities Identification Number.
        symbol: Ticker symbol if traded on an exchange.
        security_type: Type of security (e.g. 'MF', 'EQUITY').
        amfi_code: Association of Mutual Funds in India identifier.
        amc: Asset Management Company managing the scheme.
        category: Scheme investment category.
    """

    id: int
    name: str
    isin: str = ""
    symbol: str = ""
    security_type: str = ""
    amfi_code: str = ""
    amc: str | None = None
    category: str | None = None

NavPoint

Bases: FoliomanBaseModel

Single date and NAV point.

Attributes:

Name Type Description
date ConfiguredDate

Valuation date for the NAV point.

nav ConfiguredDecimal

Net Asset Value per unit on the specified date.

Source code in src/folioman_client/models.py
class NavPoint(FoliomanBaseModel):
    """Single date and NAV point.

    Attributes:
        date: Valuation date for the NAV point.
        nav: Net Asset Value per unit on the specified date.
    """

    date: ConfiguredDate
    nav: ConfiguredDecimal

FolioBalance

Bases: FoliomanBaseModel

Balance for one folio holding a security.

Attributes:

Name Type Description
number str

Folio account number.

broker str

Broker / ARN identifier associated with the folio.

folio_type str

Type classification of the folio account.

units ConfiguredDecimal

Unit balance held under this folio.

value_inr ConfiguredDecimal | None

Monetary valuation in INR for this folio.

Source code in src/folioman_client/models.py
class FolioBalance(FoliomanBaseModel):
    """Balance for one folio holding a security.

    Attributes:
        number: Folio account number.
        broker: Broker / ARN identifier associated with the folio.
        folio_type: Type classification of the folio account.
        units: Unit balance held under this folio.
        value_inr: Monetary valuation in INR for this folio.
    """

    number: str
    broker: str = ""
    folio_type: str = ""
    units: ConfiguredDecimal
    value_inr: ConfiguredDecimal | None = None

Transaction

Bases: FoliomanBaseModel

Transaction ledger record.

Attributes:

Name Type Description
id int

Unique numeric identifier for the transaction record.

investor_id int

ID of the investor who owns the holding.

security_id int

ID of the security being traded.

folio_id int | None

ID of the folio account if linked.

date ConfiguredDate

Effective transaction date.

transaction_type str

Transaction category (e.g., 'PURCHASE', 'REDEMPTION', 'SIP').

units ConfiguredDecimal

Number of units transacted.

nav_or_price ConfiguredDecimal

Unit price or NAV at which the transaction was executed.

amount ConfiguredDecimal | None

Gross transaction amount in INR.

fees ConfiguredDecimal

Fees associated with the transaction.

stamp_duty ConfiguredDecimal

Mandatory stamp duty charges.

brokerage ConfiguredDecimal

Brokerage commission charged.

currency str

ISO currency code (defaults to 'INR').

source str

Ingestion source or platform (e.g., 'CAMS', 'KFintech').

narration str

Descriptive ledger text or note.

cost_basis_complete bool

Flag indicating whether cost basis is known.

via_security str | None

Auxiliary security reference for switches.

balance ConfiguredDecimal | None

Cumulative unit balance following this transaction.

Source code in src/folioman_client/models.py
class Transaction(FoliomanBaseModel):
    """Transaction ledger record.

    Attributes:
        id: Unique numeric identifier for the transaction record.
        investor_id: ID of the investor who owns the holding.
        security_id: ID of the security being traded.
        folio_id: ID of the folio account if linked.
        date: Effective transaction date.
        transaction_type: Transaction category (e.g., 'PURCHASE', 'REDEMPTION', 'SIP').
        units: Number of units transacted.
        nav_or_price: Unit price or NAV at which the transaction was executed.
        amount: Gross transaction amount in INR.
        fees: Fees associated with the transaction.
        stamp_duty: Mandatory stamp duty charges.
        brokerage: Brokerage commission charged.
        currency: ISO currency code (defaults to 'INR').
        source: Ingestion source or platform (e.g., 'CAMS', 'KFintech').
        narration: Descriptive ledger text or note.
        cost_basis_complete: Flag indicating whether cost basis is known.
        via_security: Auxiliary security reference for switches.
        balance: Cumulative unit balance following this transaction.
    """

    id: int
    investor_id: int
    security_id: int
    folio_id: int | None = None
    date: ConfiguredDate
    transaction_type: str
    units: ConfiguredDecimal
    nav_or_price: ConfiguredDecimal
    amount: ConfiguredDecimal | None = None
    fees: ConfiguredDecimal = Decimal("0")
    stamp_duty: ConfiguredDecimal = Decimal("0")
    brokerage: ConfiguredDecimal = Decimal("0")
    currency: str = "INR"
    source: str = ""
    narration: str = ""
    cost_basis_complete: bool = True
    via_security: str | None = None
    balance: ConfiguredDecimal | None = None

SchemeDetail

Bases: FoliomanBaseModel

Detailed scheme view for an investor.

Attributes:

Name Type Description
security SchemeRef

Scheme metadata reference.

as_of ConfiguredDate

Point-in-time calculation date.

units ConfiguredDecimal

Total units held across all folios.

value_inr ConfiguredDecimal | None

Current market value in INR.

invested_inr ConfiguredDecimal | None

Total cost basis in INR.

return_pct float | None

Absolute return percentage.

xirr float | None

Annualized internal rate of return.

xirr_status str

Status indicator for XIRR calculation convergence.

day_change_inr ConfiguredDecimal | None

Valuation change compared to previous trading day.

day_change_pct float | None

Percentage change compared to previous trading day.

latest_nav ConfiguredDecimal | None

Most recent recorded Net Asset Value.

latest_nav_date ConfiguredDate | None

Date of the latest NAV record.

has_transactions bool

Whether transaction records are available.

partial_history bool

Whether historical records are incomplete.

partial_history_from ConfiguredDate | None

Starting date of available history if partial.

folios list[FolioBalance]

Breakdown of units across individual folios.

nav_history list[NavPoint]

Time series of historical NAV points.

transactions list[Transaction]

Ledger of historical transactions for this scheme.

Source code in src/folioman_client/models.py
class SchemeDetail(FoliomanBaseModel):
    """Detailed scheme view for an investor.

    Attributes:
        security: Scheme metadata reference.
        as_of: Point-in-time calculation date.
        units: Total units held across all folios.
        value_inr: Current market value in INR.
        invested_inr: Total cost basis in INR.
        return_pct: Absolute return percentage.
        xirr: Annualized internal rate of return.
        xirr_status: Status indicator for XIRR calculation convergence.
        day_change_inr: Valuation change compared to previous trading day.
        day_change_pct: Percentage change compared to previous trading day.
        latest_nav: Most recent recorded Net Asset Value.
        latest_nav_date: Date of the latest NAV record.
        has_transactions: Whether transaction records are available.
        partial_history: Whether historical records are incomplete.
        partial_history_from: Starting date of available history if partial.
        folios: Breakdown of units across individual folios.
        nav_history: Time series of historical NAV points.
        transactions: Ledger of historical transactions for this scheme.
    """

    security: SchemeRef
    as_of: ConfiguredDate
    units: ConfiguredDecimal
    value_inr: ConfiguredDecimal | None = None
    invested_inr: ConfiguredDecimal | None = None
    return_pct: float | None = None
    xirr: float | None = None
    xirr_status: str = ""
    day_change_inr: ConfiguredDecimal | None = None
    day_change_pct: float | None = None
    latest_nav: ConfiguredDecimal | None = None
    latest_nav_date: ConfiguredDate | None = None
    has_transactions: bool = False
    partial_history: bool = False
    partial_history_from: ConfiguredDate | None = None
    folios: list[FolioBalance] = Field(default_factory=list)
    nav_history: list[NavPoint] = Field(default_factory=list)
    transactions: list[Transaction] = Field(default_factory=list)

AssetMixRow

Bases: FoliomanBaseModel

Allocation breakdown row by security type.

Attributes:

Name Type Description
security_type str

Asset class label (e.g. 'EQUITY', 'DEBT', 'CASH').

value_inr ConfiguredDecimal

Total valuation allocated to this security type in INR.

Source code in src/folioman_client/models.py
class AssetMixRow(FoliomanBaseModel):
    """Allocation breakdown row by security type.

    Attributes:
        security_type: Asset class label (e.g. 'EQUITY', 'DEBT', 'CASH').
        value_inr: Total valuation allocated to this security type in INR.
    """

    security_type: str
    value_inr: ConfiguredDecimal

AllocationBucket

Bases: FoliomanBaseModel

Allocation breakdown row by AMC or category.

Attributes:

Name Type Description
label str

AMC name or category classification label.

value_inr ConfiguredDecimal

Total valuation allocated to this bucket in INR.

Source code in src/folioman_client/models.py
class AllocationBucket(FoliomanBaseModel):
    """Allocation breakdown row by AMC or category.

    Attributes:
        label: AMC name or category classification label.
        value_inr: Total valuation allocated to this bucket in INR.
    """

    label: str
    value_inr: ConfiguredDecimal

PeriodReturn

Bases: FoliomanBaseModel

Trailing window money-weighted return (1M, 1Y, All, etc.).

Attributes:

Name Type Description
period str

Label for the trailing window (e.g. '1M', '3M', '1Y', 'ALL').

annualized float

Annualized internal rate of return for the period.

absolute float | None

Absolute percentage return for the period.

days int

Number of calendar days in the evaluation window.

Source code in src/folioman_client/models.py
class PeriodReturn(FoliomanBaseModel):
    """Trailing window money-weighted return (1M, 1Y, All, etc.).

    Attributes:
        period: Label for the trailing window (e.g. '1M', '3M', '1Y', 'ALL').
        annualized: Annualized internal rate of return for the period.
        absolute: Absolute percentage return for the period.
        days: Number of calendar days in the evaluation window.
    """

    period: str
    annualized: float
    absolute: float | None = None
    days: int

PortfolioSummary

Bases: FoliomanBaseModel

Overall portfolio summary for an investor (InvestorSummaryOut).

Attributes:

Name Type Description
investor_id int

Unique identifier of the investor.

as_of ConfiguredDate

Valuation date of the portfolio summary.

total_inr ConfiguredDecimal

Aggregate portfolio valuation in INR.

is_provisional bool

Flag indicating if pricing is provisional or final.

navs_as_of ConfiguredDate | None

Effective date of NAV points used in this valuation.

navs_stale bool

True if latest NAVs have not been updated recently.

holdings_count int

Total count of active holdings.

integrity_unit_count int

Count of holdings with verified unit balances.

tax_ready_count int

Count of holdings with reconciled tax lots.

needs_attention_count int

Holdings requiring advisor intervention.

snapshot_count int

Number of historical snapshots available.

stale_count int

Number of unpriced or stale holdings.

unpriced_fund_count int

Number of holdings without available NAV.

last_import_at ConfiguredDatetime | None

Timestamp of the most recent data import.

day_change_inr ConfiguredDecimal | None

Monetary change since previous business day.

xirr float | None

Overall portfolio annualized internal rate of return.

period_returns list[PeriodReturn]

Trailing performance metrics across windows.

asset_mix list[AssetMixRow]

Asset class breakdown (Equity, Debt, Cash, etc.).

amc_mix list[AllocationBucket]

Asset Management Company distribution breakdown.

category_mix list[AllocationBucket]

Mutual fund category distribution breakdown.

top_holdings list[Holding]

Subset of top holdings by value.

holdings list[Holding]

Full list of priced holdings for this investor.

Source code in src/folioman_client/models.py
class PortfolioSummary(FoliomanBaseModel):
    """Overall portfolio summary for an investor (InvestorSummaryOut).

    Attributes:
        investor_id: Unique identifier of the investor.
        as_of: Valuation date of the portfolio summary.
        total_inr: Aggregate portfolio valuation in INR.
        is_provisional: Flag indicating if pricing is provisional or final.
        navs_as_of: Effective date of NAV points used in this valuation.
        navs_stale: True if latest NAVs have not been updated recently.
        holdings_count: Total count of active holdings.
        integrity_unit_count: Count of holdings with verified unit balances.
        tax_ready_count: Count of holdings with reconciled tax lots.
        needs_attention_count: Holdings requiring advisor intervention.
        snapshot_count: Number of historical snapshots available.
        stale_count: Number of unpriced or stale holdings.
        unpriced_fund_count: Number of holdings without available NAV.
        last_import_at: Timestamp of the most recent data import.
        day_change_inr: Monetary change since previous business day.
        xirr: Overall portfolio annualized internal rate of return.
        period_returns: Trailing performance metrics across windows.
        asset_mix: Asset class breakdown (Equity, Debt, Cash, etc.).
        amc_mix: Asset Management Company distribution breakdown.
        category_mix: Mutual fund category distribution breakdown.
        top_holdings: Subset of top holdings by value.
        holdings: Full list of priced holdings for this investor.
    """

    investor_id: int
    as_of: ConfiguredDate
    total_inr: ConfiguredDecimal
    is_provisional: bool = False
    navs_as_of: ConfiguredDate | None = None
    navs_stale: bool = False
    holdings_count: int = 0
    integrity_unit_count: int = 0
    tax_ready_count: int = 0
    needs_attention_count: int = 0
    snapshot_count: int = 0
    stale_count: int = 0
    unpriced_fund_count: int = 0
    last_import_at: ConfiguredDatetime | None = None
    day_change_inr: ConfiguredDecimal | None = None
    xirr: float | None = None
    period_returns: list[PeriodReturn] = Field(default_factory=list)
    asset_mix: list[AssetMixRow] = Field(default_factory=list)
    amc_mix: list[AllocationBucket] = Field(default_factory=list)
    category_mix: list[AllocationBucket] = Field(default_factory=list)
    top_holdings: list[Holding] = Field(default_factory=list)
    holdings: list[Holding] = Field(default_factory=list)

ValueSeriesPoint

Bases: FoliomanBaseModel

Single date point in net worth valuation series.

Attributes:

Name Type Description
date ConfiguredDate

Valuation point date.

value_inr ConfiguredDecimal

Total portfolio value in INR on this date.

invested_inr ConfiguredDecimal

Cumulative invested capital in INR on this date.

stale bool

Whether the valuation data for this date is stale.

Source code in src/folioman_client/models.py
class ValueSeriesPoint(FoliomanBaseModel):
    """Single date point in net worth valuation series.

    Attributes:
        date: Valuation point date.
        value_inr: Total portfolio value in INR on this date.
        invested_inr: Cumulative invested capital in INR on this date.
        stale: Whether the valuation data for this date is stale.
    """

    date: ConfiguredDate
    value_inr: ConfiguredDecimal
    invested_inr: ConfiguredDecimal
    stale: bool = False

ValueSeries

Bases: FoliomanBaseModel

Reconstructed net-worth-over-time time series.

Attributes:

Name Type Description
investor_id int | None

Optional investor ID filter.

family_id int | None

Optional family ID filter.

start ConfiguredDate

Start date of the time series.

end ConfiguredDate

End date of the time series.

granularity str

Sampling frequency ('daily', 'weekly', 'monthly').

points list[ValueSeriesPoint]

Ordered list of valuation time series points.

Source code in src/folioman_client/models.py
class ValueSeries(FoliomanBaseModel):
    """Reconstructed net-worth-over-time time series.

    Attributes:
        investor_id: Optional investor ID filter.
        family_id: Optional family ID filter.
        start: Start date of the time series.
        end: End date of the time series.
        granularity: Sampling frequency ('daily', 'weekly', 'monthly').
        points: Ordered list of valuation time series points.
    """

    investor_id: int | None = None
    family_id: int | None = None
    start: ConfiguredDate
    end: ConfiguredDate
    granularity: str
    points: list[ValueSeriesPoint] = Field(default_factory=list)

ValuationStatus

Bases: FoliomanBaseModel

Valuation calculation readiness status.

Attributes:

Name Type Description
investor_id int | None

ID of the investor.

family_id int | None

ID of the family group if applicable.

status str

Engine readiness status (e.g. 'READY', 'COMPUTING', 'ERROR').

computed_through ConfiguredDate | None

Date up to which valuations have been finalized.

recompute_from ConfiguredDate | None

Earliest date from which recalculation is needed.

is_provisional bool

Whether the current numbers are provisional.

Source code in src/folioman_client/models.py
class ValuationStatus(FoliomanBaseModel):
    """Valuation calculation readiness status.

    Attributes:
        investor_id: ID of the investor.
        family_id: ID of the family group if applicable.
        status: Engine readiness status (e.g. 'READY', 'COMPUTING', 'ERROR').
        computed_through: Date up to which valuations have been finalized.
        recompute_from: Earliest date from which recalculation is needed.
        is_provisional: Whether the current numbers are provisional.
    """

    investor_id: int | None = None
    family_id: int | None = None
    status: str
    computed_through: ConfiguredDate | None = None
    recompute_from: ConfiguredDate | None = None
    is_provisional: bool = False

CapitalGainRow

Bases: FoliomanBaseModel

One realised disposal lot in capital gains report.

Attributes:

Name Type Description
security_id int | None

ID of the security redeemed or sold.

name str

Name of the security or mutual fund scheme.

isin str

ISIN code of the security.

units ConfiguredDecimal

Number of units redeemed or disposed.

sale_value ConfiguredDecimal

Realized sale proceeds in INR.

cost ConfiguredDecimal

Indexed or purchase cost basis in INR.

gain ConfiguredDecimal

Realized capital gain or loss in INR.

term str

Classification of gain ('STCG' or 'LTCG').

acquired_on ConfiguredDate

Original purchase date of the lot.

sold_on ConfiguredDate

Date of disposal or redemption.

grandfathering_unavailable bool

Whether Section 112A grandfathering is unavailable.

Source code in src/folioman_client/models.py
class CapitalGainRow(FoliomanBaseModel):
    """One realised disposal lot in capital gains report.

    Attributes:
        security_id: ID of the security redeemed or sold.
        name: Name of the security or mutual fund scheme.
        isin: ISIN code of the security.
        units: Number of units redeemed or disposed.
        sale_value: Realized sale proceeds in INR.
        cost: Indexed or purchase cost basis in INR.
        gain: Realized capital gain or loss in INR.
        term: Classification of gain ('STCG' or 'LTCG').
        acquired_on: Original purchase date of the lot.
        sold_on: Date of disposal or redemption.
        grandfathering_unavailable: Whether Section 112A grandfathering is unavailable.
    """

    security_id: int | None = None
    name: str
    isin: str = ""
    units: ConfiguredDecimal
    sale_value: ConfiguredDecimal
    cost: ConfiguredDecimal
    gain: ConfiguredDecimal
    term: str
    acquired_on: ConfiguredDate
    sold_on: ConfiguredDate
    grandfathering_unavailable: bool = False

CapitalGainsReport

Bases: FoliomanBaseModel

Realised capital gains report for a financial year (CapitalGainsOut).

Attributes:

Name Type Description
fy str

Financial year label (e.g., '2024-25').

stcg_total ConfiguredDecimal

Total realized Short-Term Capital Gains in INR.

ltcg_total ConfiguredDecimal

Total realized Long-Term Capital Gains in INR.

rows list[CapitalGainRow]

Detailed breakdown of individual disposal lots.

disclaimer str

Legal or regulatory tax disclaimer text.

Source code in src/folioman_client/models.py
class CapitalGainsReport(FoliomanBaseModel):
    """Realised capital gains report for a financial year (CapitalGainsOut).

    Attributes:
        fy: Financial year label (e.g., '2024-25').
        stcg_total: Total realized Short-Term Capital Gains in INR.
        ltcg_total: Total realized Long-Term Capital Gains in INR.
        rows: Detailed breakdown of individual disposal lots.
        disclaimer: Legal or regulatory tax disclaimer text.
    """

    fy: str
    stcg_total: ConfiguredDecimal
    ltcg_total: ConfiguredDecimal
    rows: list[CapitalGainRow] = Field(default_factory=list)
    disclaimer: str = ""

CapitalGainsFyPoint

Bases: FoliomanBaseModel

Year-over-year capital gains summary point.

Attributes:

Name Type Description
fy str

Financial year label (e.g., '2023-24').

stcg ConfiguredDecimal

Total realized STCG for the year.

ltcg ConfiguredDecimal

Total realized LTCG for the year.

Source code in src/folioman_client/models.py
class CapitalGainsFyPoint(FoliomanBaseModel):
    """Year-over-year capital gains summary point.

    Attributes:
        fy: Financial year label (e.g., '2023-24').
        stcg: Total realized STCG for the year.
        ltcg: Total realized LTCG for the year.
    """

    fy: str
    stcg: ConfiguredDecimal
    ltcg: ConfiguredDecimal

options: show_root_heading: true


Errors

folioman_client.errors

Exception hierarchy for the Folioman client.

Keep exceptions focused, practical, and small.

FoliomanError

Bases: Exception

Base exception for all Folioman client errors.

Source code in src/folioman_client/errors.py
class FoliomanError(Exception):
    """Base exception for all Folioman client errors."""

FoliomanAuthError

Bases: FoliomanError

Raised when authentication fails (invalid credentials, expired/rejected token refresh).

Source code in src/folioman_client/errors.py
class FoliomanAuthError(FoliomanError):
    """Raised when authentication fails (invalid credentials, expired/rejected token refresh)."""

FoliomanNotFoundError

Bases: FoliomanError

Raised when the requested resource is not found (HTTP 404).

Source code in src/folioman_client/errors.py
class FoliomanNotFoundError(FoliomanError):
    """Raised when the requested resource is not found (HTTP 404)."""

FoliomanAPIError

Bases: FoliomanError

Raised when the Folioman API returns an error response (HTTP 4xx/5xx).

Attributes:

Name Type Description
status_code

The HTTP status code returned by the server.

response_data

Parsed response payload or raw error details, if available.

Source code in src/folioman_client/errors.py
class FoliomanAPIError(FoliomanError):
    """Raised when the Folioman API returns an error response (HTTP 4xx/5xx).

    Attributes:
        status_code: The HTTP status code returned by the server.
        response_data: Parsed response payload or raw error details, if available.
    """

    def __init__(
        self,
        message: str,
        status_code: int,
        response_data: Any | None = None,
    ) -> None:
        """Initialize FoliomanAPIError with message, status code, and optional payload.

        Args:
            message: Human-readable description of the error.
            status_code: HTTP response status code (e.g., 400, 500).
            response_data: Deserialized JSON payload or raw text error from the server.
        """
        super().__init__(message)
        self.status_code = status_code
        self.response_data = response_data

    def __str__(self) -> str:
        base = super().__str__()
        if self.response_data:
            return f"[{self.status_code}] {base} - {self.response_data}"
        return f"[{self.status_code}] {base}"

__init__

__init__(message: str, status_code: int, response_data: Any | None = None) -> None

Initialize FoliomanAPIError with message, status code, and optional payload.

Parameters:

Name Type Description Default
message str

Human-readable description of the error.

required
status_code int

HTTP response status code (e.g., 400, 500).

required
response_data Any | None

Deserialized JSON payload or raw text error from the server.

None
Source code in src/folioman_client/errors.py
def __init__(
    self,
    message: str,
    status_code: int,
    response_data: Any | None = None,
) -> None:
    """Initialize FoliomanAPIError with message, status code, and optional payload.

    Args:
        message: Human-readable description of the error.
        status_code: HTTP response status code (e.g., 400, 500).
        response_data: Deserialized JSON payload or raw text error from the server.
    """
    super().__init__(message)
    self.status_code = status_code
    self.response_data = response_data

options: show_root_heading: true


Types

folioman_client.types

Reusable Pydantic Annotated types for Folioman models.

ConfiguredDecimal module-attribute

ConfiguredDecimal = Annotated[Decimal, PlainSerializer(lambda x: float(x), return_type=float, when_used='unless-none')]

Decimal type serialized to float unless None.

ConfiguredDate module-attribute

ConfiguredDate = Annotated[date, PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used='unless-none')]

Date type serialized to ISO-8601 string (YYYY-MM-DD) unless None.

ConfiguredDatetime module-attribute

ConfiguredDatetime = Annotated[datetime, PlainSerializer(lambda x: x.isoformat(), return_type=str, when_used='unless-none')]

Datetime type serialized to ISO-8601 string unless None.

options: show_root_heading: true