Repository navigation
feat(account-tree): add usage attribution types and attributed usage #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ | |
| FindAllCommandMixin, | ||
| FindCommandMixin, | ||
| ) | ||
| from ..models.attributed_usage import AttributedUsageResponse | ||
| from ..models.customer import CustomerResponse | ||
| from ..models.customer_projected_usage import CustomerProjectedUsageResponse | ||
| from ..models.customer_usage import CustomerUsageResponse | ||
|
|
@@ -147,6 +148,57 @@ def past_usage( | |
| data=get_response_data(response=api_response), | ||
| ) | ||
|
|
||
| def attributed_usage( | ||
| self, | ||
| resource_id: str, | ||
| external_subscription_id: str, | ||
| group_by: str, | ||
| filters: Optional[Mapping[str, Union[str, list[str]]]] = None, | ||
| charge_codes: Optional[list[str]] = None, | ||
| options: QueryPairs = None, | ||
| ) -> Mapping[str, Any]: | ||
| query_params: dict[str, Union[int, str, list[str]]] = { | ||
| "external_subscription_id": external_subscription_id, | ||
| "group_by": group_by, | ||
| } | ||
| if filters is not None: | ||
| for k, v in filters.items(): | ||
|
lovrocolic marked this conversation as resolved.
Resolved
|
||
| if isinstance(v, str): | ||
| query_params[f"filters[{k}]"] = v | ||
| elif not v: | ||
| # An empty list would send no param and silently drop the filter | ||
| raise ValueError(f"filters[{k}] must not be empty") | ||
| else: | ||
| query_params[f"filters[{k}][]"] = list(v) | ||
| if charge_codes is not None: | ||
| if not charge_codes: | ||
| # An empty list would send no param, which the API reads as all charges | ||
| raise ValueError("charge_codes must not be empty") | ||
| query_params["charge_codes[]"] = list(charge_codes) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A single string passed here, |
||
| if options is not None: | ||
| query_params.update(options) | ||
|
|
||
| api_response: Response = send_get_request( | ||
| url=make_url( | ||
| origin=self.base_url, | ||
| path_parts=(self.API_RESOURCE, resource_id, "attributed_usage"), | ||
| query_pairs=query_params, | ||
| ), | ||
| headers=make_headers(api_key=self.api_key), | ||
| ) | ||
|
|
||
| response_data = get_response_data(response=api_response) | ||
| if not isinstance(response_data, Mapping): | ||
| response_data = {} | ||
|
|
||
| return { | ||
| "attributed_usage": prepare_object_response( | ||
| response_model=AttributedUsageResponse, | ||
| data=response_data.get("attributed_usage"), | ||
| ), | ||
| "meta": response_data.get("meta"), | ||
| } | ||
|
|
||
| def portal_url(self, resource_id: str) -> str: | ||
| api_response: Response = send_get_request( | ||
| url=make_url( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from typing import Dict, List, Optional | ||
|
|
||
| from ..base_model import BaseModel, BaseResponseModel | ||
|
|
||
|
|
||
| class AttributedChargeUsage(BaseModel): | ||
| lago_charge_id: str | ||
| charge_code: str | ||
| billable_metric_code: str | ||
| lago_charge_filter_id: Optional[str] | ||
| charge_filter_values: Optional[Dict[str, List[str]]] | ||
| charge_filter_invoice_display_name: Optional[str] | ||
| units: str | ||
| amount_cents: Optional[int] | ||
| precise_amount_cents: Optional[str] | ||
| events_count: int | ||
|
|
||
|
|
||
| class AttributedUsageAggregate(BaseModel): | ||
| amount_cents: Optional[int] | ||
| precise_amount_cents: Optional[str] | ||
| events_count: int | ||
| charges_usage: List[AttributedChargeUsage] | ||
|
|
||
|
|
||
| class AttributedUsageRow(AttributedUsageAggregate): | ||
| value: Optional[str] | ||
| rank: int | ||
|
|
||
|
|
||
| class AttributedUsageResponse(BaseResponseModel): | ||
| lago_subscription_id: str | ||
| external_subscription_id: str | ||
| group_by: Optional[str] | ||
| basis: str | ||
| from_datetime: str | ||
| to_datetime: str | ||
| currency: str | ||
| split_filters_count: Optional[int] | ||
| rows: List[AttributedUsageRow] | ||
| unattributed: AttributedUsageAggregate | ||
| totals: AttributedUsageAggregate |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| from typing import List, Optional | ||
|
|
||
| from ..base_model import BaseModel, BaseResponseModel | ||
|
|
||
|
|
||
| class UsageAttributionType(BaseModel): | ||
| code: Optional[str] | ||
| name: Optional[str] | ||
| description: Optional[str] | ||
| role: Optional[str] | ||
| parent_code: Optional[str] | ||
| attribution_keys: Optional[List[str]] | ||
|
|
||
|
|
||
| class UsageAttributionTypeResponse(BaseResponseModel): | ||
| lago_id: str | ||
| lago_organization_id: str | ||
| code: str | ||
| name: Optional[str] | ||
| description: Optional[str] | ||
| attribution_keys: List[str] | ||
| role: str | ||
| lago_parent_id: Optional[str] | ||
| parent_code: Optional[str] | ||
| created_at: str | ||
| updated_at: str | ||
|
|
||
|
|
||
| class UsageAttributionTypesResponse(BaseResponseModel): | ||
| __root__: List[UsageAttributionTypeResponse] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| from typing import ClassVar, Type | ||
|
|
||
| from ..base_client import BaseClient | ||
| from ..mixins import ( | ||
| CreateCommandMixin, | ||
| DestroyCommandMixin, | ||
| FindAllCommandMixin, | ||
| FindCommandMixin, | ||
| UpdateCommandMixin, | ||
| ) | ||
| from ..models.usage_attribution_type import UsageAttributionTypeResponse | ||
|
|
||
|
|
||
| class UsageAttributionTypeClient( | ||
| CreateCommandMixin[UsageAttributionTypeResponse], | ||
| DestroyCommandMixin[UsageAttributionTypeResponse], | ||
| FindAllCommandMixin[UsageAttributionTypeResponse], | ||
| FindCommandMixin[UsageAttributionTypeResponse], | ||
| UpdateCommandMixin[UsageAttributionTypeResponse], | ||
| BaseClient, | ||
| ): | ||
| API_RESOURCE: ClassVar[str] = "usage_attribution_types" | ||
| RESPONSE_MODEL: ClassVar[Type[UsageAttributionTypeResponse]] = UsageAttributionTypeResponse | ||
| ROOT_NAME: ClassVar[str] = "usage_attribution_type" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| { | ||
| "attributed_usage": { | ||
| "lago_subscription_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", | ||
| "external_subscription_id": "sub_123", | ||
| "group_by": "team", | ||
| "basis": "units", | ||
| "from_datetime": "2026-10-01T00:00:00Z", | ||
| "to_datetime": "2026-10-31T23:59:59Z", | ||
| "currency": "EUR", | ||
| "split_filters_count": null, | ||
| "rows": [ | ||
| { | ||
| "value": "eng", | ||
| "rank": 1, | ||
| "amount_cents": 1200, | ||
| "precise_amount_cents": "1200.4", | ||
| "events_count": 12, | ||
| "charges_usage": [ | ||
| { | ||
| "lago_charge_id": "1a901a90-1a90-1a90-1a90-1a901a901a94", | ||
| "charge_code": "tokens", | ||
| "billable_metric_code": "tokens", | ||
| "lago_charge_filter_id": "1a901a90-1a90-1a90-1a90-1a901a901a95", | ||
| "charge_filter_values": {"model": ["opus"]}, | ||
| "charge_filter_invoice_display_name": "Opus tokens", | ||
| "units": "120.0", | ||
| "amount_cents": 1200, | ||
| "precise_amount_cents": "1200.4", | ||
| "events_count": 12 | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "unattributed": { | ||
| "amount_cents": 100, | ||
| "precise_amount_cents": "100.0", | ||
| "events_count": 1, | ||
| "charges_usage": [ | ||
| { | ||
| "lago_charge_id": "1a901a90-1a90-1a90-1a90-1a901a901a94", | ||
| "charge_code": "tokens", | ||
| "billable_metric_code": "tokens", | ||
| "lago_charge_filter_id": null, | ||
| "charge_filter_values": null, | ||
| "charge_filter_invoice_display_name": null, | ||
| "units": "10.0", | ||
| "amount_cents": 100, | ||
| "precise_amount_cents": "100.0", | ||
| "events_count": 1 | ||
| } | ||
| ] | ||
| }, | ||
| "totals": { | ||
| "amount_cents": 1300, | ||
| "precise_amount_cents": "1300.4", | ||
| "events_count": 13, | ||
| "charges_usage": [ | ||
| { | ||
| "lago_charge_id": "1a901a90-1a90-1a90-1a90-1a901a901a94", | ||
| "charge_code": "tokens", | ||
| "billable_metric_code": "tokens", | ||
| "lago_charge_filter_id": null, | ||
| "charge_filter_values": null, | ||
| "charge_filter_invoice_display_name": null, | ||
| "units": "130.0", | ||
| "amount_cents": 1300, | ||
| "precise_amount_cents": "1300.4", | ||
| "events_count": 13 | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| "meta": { | ||
| "current_page": 1, | ||
| "next_page": null, | ||
| "prev_page": null, | ||
| "total_pages": 1, | ||
| "total_count": 1 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "usage_attribution_type": { | ||
| "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", | ||
| "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a91", | ||
| "code": "team", | ||
| "name": "Team", | ||
| "description": "Teams of the customer", | ||
| "attribution_keys": ["team_id"], | ||
| "role": "hierarchical", | ||
| "lago_parent_id": "1a901a90-1a90-1a90-1a90-1a901a901a92", | ||
| "parent_code": "department", | ||
| "created_at": "2026-10-01T08:59:51Z", | ||
| "updated_at": "2026-10-01T08:59:51Z" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| { | ||
| "usage_attribution_types": [ | ||
| { | ||
| "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a92", | ||
| "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a91", | ||
| "code": "department", | ||
| "name": "Department", | ||
| "description": null, | ||
| "attribution_keys": ["department_id"], | ||
| "role": "hierarchical", | ||
| "lago_parent_id": null, | ||
| "parent_code": null, | ||
| "created_at": "2026-10-01T08:59:51Z", | ||
| "updated_at": "2026-10-01T08:59:51Z" | ||
| }, | ||
| { | ||
| "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a93", | ||
| "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a91", | ||
| "code": "model", | ||
| "name": "Model", | ||
| "description": "LLM model", | ||
| "attribution_keys": ["model", "model_name"], | ||
| "role": "flat", | ||
| "lago_parent_id": null, | ||
| "parent_code": null, | ||
| "created_at": "2026-10-01T08:59:51Z", | ||
| "updated_at": "2026-10-01T08:59:51Z" | ||
| } | ||
| ], | ||
| "meta": { | ||
| "current_page": 1, | ||
| "next_page": null, | ||
| "prev_page": null, | ||
| "total_pages": 1, | ||
| "total_count": 2 | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.