diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d74559f..fb7de62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - run: pip install poetry==${{ env.POETRY_VERSION }} @@ -53,7 +53,7 @@ jobs: org_id: TESTING_STAGING_ORG_ID steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: pip install poetry==${{ env.POETRY_VERSION }} @@ -72,7 +72,7 @@ jobs: - test-integration steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - run: pip install poetry==${{ env.POETRY_VERSION }} diff --git a/README.md b/README.md index e1ef381..14446e0 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ -![axiom-py: The official Python bindings for the Axiom API](.github/images/banner-dark.svg#gh-dark-mode-only) -![axiom-py: The official Python bindings for the Axiom API](.github/images/banner-light.svg#gh-light-mode-only) - -
+# axiom-py + + + + + + + Axiom.co banner + + +  [![CI][ci_badge]][ci] [![PyPI version][pypi_badge]][pypi] [![Python version][version_badge]][pypi] -
- [Axiom](https://axiom.co) unlocks observability at any scale. - **Ingest with ease, store without limits:** Axiom’s next-generation datastore enables ingesting petabytes of data with ultimate efficiency. Ship logs from Kubernetes, AWS, Azure, Google Cloud, DigitalOcean, Nomad, and others. diff --git a/axiom/client.py b/axiom/client.py index 6dfa59e..5897aea 100644 --- a/axiom/client.py +++ b/axiom/client.py @@ -10,7 +10,7 @@ from .util import Util from enum import Enum from humps import decamelize -from typing import Optional, List, Dict, Any +from typing import Optional, List, Dict from logging import getLogger from dataclasses import asdict, dataclass, field from datetime import datetime @@ -100,11 +100,18 @@ class WrongQueryKindException(Exception): class AplOptions: """AplOptions specifies the optional parameters for the apl query method.""" + # Start time for the interval to query. start_time: Optional[datetime] = field(default=None) + # End time for the interval to query. end_time: Optional[datetime] = field(default=None) - no_cache: bool = field(default=False) - save: bool = field(default=False) + # The result format. format: AplResultFormat = field(default=AplResultFormat.Legacy) + # Cursor is the query cursor. It should be set to the Cursor returned with + # a previous query result if it was partial. + cursor: Optional[str] = field(default=None) + # IncludeCursor will return the Cursor as part of the query result, if set + # to true. + includeCursor: bool = field(default=False) def raise_response_error(r): @@ -273,11 +280,8 @@ def query(self, apl: str, opts: Optional[AplOptions] = None) -> QueryResult: def create_api_token(self, opts: TokenAttributes) -> Token: """Creates a new API token with permissions specified in a TokenAttributes object.""" res = self.session.post( - '/v2/tokens', - data=ujson.dumps( - asdict(opts), - default=Util.handle_json_serialization - ) + "/v2/tokens", + data=ujson.dumps(asdict(opts), default=Util.handle_json_serialization), ) # Return the new token and ID. @@ -286,9 +290,9 @@ def create_api_token(self, opts: TokenAttributes) -> Token: def delete_api_token(self, token_id: str) -> None: """Delete an API token using its ID string.""" - self.session.delete(f'/v2/tokens/{token_id}') + self.session.delete(f"/v2/tokens/{token_id}") - def _prepare_query_options(self, opts: QueryOptions) -> Dict[str, Any]: + def _prepare_query_options(self, opts: QueryOptions) -> Dict[str, object]: """returns the query options as a Dict, handles any renaming for key fields.""" if opts is None: return {} @@ -304,7 +308,9 @@ def _prepare_query_options(self, opts: QueryOptions) -> Dict[str, Any]: return params - def _prepare_ingest_options(self, opts: Optional[IngestOptions]) -> Dict[str, Any]: + def _prepare_ingest_options( + self, opts: Optional[IngestOptions] + ) -> Dict[str, object]: """the query params for ingest api are expected in a format that couldn't be defined as a variable name because it has a dash. As a work around, we create the params dict manually.""" @@ -322,34 +328,31 @@ def _prepare_ingest_options(self, opts: Optional[IngestOptions]) -> Dict[str, An return params - def _prepare_apl_options(self, opts: Optional[AplOptions]) -> Dict[str, Any]: + def _prepare_apl_options(self, opts: Optional[AplOptions]) -> Dict[str, object]: """Prepare the apl query options for the request.""" - params = {} + params = {"format": AplResultFormat.Legacy.value} - if opts is None: - params["format"] = AplResultFormat.Legacy.value - return params - - if opts.no_cache: - params["nocache"] = opts.no_cache.__str__() - if opts.save: - params["save"] = opts.save - if opts.format: - params["format"] = opts.format.value + if opts is not None: + if opts.format: + params["format"] = opts.format.value return params def _prepare_apl_payload( self, apl: str, opts: Optional[AplOptions] - ) -> Dict[str, Any]: + ) -> Dict[str, object]: """Prepare the apl query options for the request.""" params = {} params["apl"] = apl if opts is not None: - if opts.start_time: + if opts.start_time is not None: params["startTime"] = opts.start_time - if opts.end_time: + if opts.end_time is not None: params["endTime"] = opts.end_time + if opts.cursor is not None: + params["cursor"] = opts.cursor + if opts.includeCursor: + params["includeCursor"] = opts.includeCursor return params diff --git a/axiom/query/result.py b/axiom/query/result.py index 3917230..95a585c 100644 --- a/axiom/query/result.py +++ b/axiom/query/result.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta -from typing import List, Dict, Any, Optional +from typing import List, Dict, Optional from enum import Enum from .query import QueryLegacy @@ -87,7 +87,7 @@ class Entry: _rowId: str # contains the raw data of the event (with filters and aggregations # applied). - data: Dict[str, Any] + data: Dict[str, object] @dataclass @@ -96,7 +96,7 @@ class EntryGroupAgg: # alias is the aggregations alias. If it wasn't specified at query time, it # is the uppercased string representation of the aggregation operation. - value: Any + value: object op: str = field(default="") # value is the result value of the aggregation. @@ -108,7 +108,7 @@ class EntryGroup: # the unique id of the group. id: int # group maps the fieldnames to the unique values for the entry. - group: Dict[str, Any] + group: Dict[str, object] # aggregations of the group. aggregations: List[EntryGroupAgg] diff --git a/axiom/tokens.py b/axiom/tokens.py index 4ab8369..fc8fdf3 100644 --- a/axiom/tokens.py +++ b/axiom/tokens.py @@ -1,9 +1,10 @@ -from dataclasses import dataclass -from typing import Literal +from dataclasses import dataclass, field +from typing import Literal, Optional @dataclass class TokenDatasetCapabilities: + # pylint: disable=unsubscriptable-object """ TokenDatasetCapabilities describes the dataset-level permissions which a token can be assigned. @@ -12,52 +13,80 @@ class TokenDatasetCapabilities: """ # Ability to ingest data. Optional. - ingest: list[Literal["create"]] | None = None + ingest: Optional[list[Literal["create"]]] = field(default=None) # Ability to query data. Optional. - query: list[Literal["read"]] | None = None + query: Optional[list[Literal["read"]]] = field(default=None) # Ability to use starred queries. Optional. - starredQueries: list[Literal["create", "read", "update", "delete"]] | None = None + starredQueries: Optional[list[Literal["create", "read", "update", "delete"]]] = ( + field(default=None) + ) # Ability to use virtual fields. Optional. - virtualFields: list[Literal["create", "read", "update", "delete"]] | None = None + virtualFields: Optional[list[Literal["create", "read", "update", "delete"]]] = ( + field(default=None) + ) @dataclass class TokenOrganizationCapabilities: + # pylint: disable=unsubscriptable-object """ TokenOrganizationCapabilities describes the org-level permissions which a token can be assigned. """ # Ability to use annotations. Optional. - annotations: list[Literal["create", "read", "update", "delete"]] | None = None + annotations: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use api tokens. Optional. - apiTokens: list[Literal["create", "read", "update", "delete"]] | None = None + apiTokens: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to access billing. Optional. - billing: list[Literal["read", "update"]] | None = None + billing: Optional[list[Literal["read", "update"]]] = field(default=None) # Ability to use dashboards. Optional. - dashboards: list[Literal["create", "read", "update", "delete"]] | None = None + dashboards: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use datasets. Optional. - datasets: list[Literal["create", "read", "update", "delete"]] | None = None + datasets: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use endpoints. Optional. - endpoints: list[Literal["create", "read", "update", "delete"]] | None = None + endpoints: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use flows. Optional. - flows: list[Literal["create", "read", "update", "delete"]] | None = None + flows: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use integrations. Optional. - integrations: list[Literal["create", "read", "update", "delete"]] | None = None + integrations: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use monitors. Optional. - monitors: list[Literal["create", "read", "update", "delete"]] | None = None + monitors: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use notifiers. Optional. - notifiers: list[Literal["create", "read", "update", "delete"]] | None = None + notifiers: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use role-based access controls. Optional. - rbac: list[Literal["create", "read", "update", "delete"]] | None = None + rbac: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) # Ability to use shared access keys. Optional. - sharedAccessKeys: list[Literal["read", "update"]] | None = None + sharedAccessKeys: Optional[list[Literal["read", "update"]]] = field(default=None) # Ability to use users. Optional. - users: list[Literal["create", "read", "update", "delete"]] | None = None + users: Optional[list[Literal["create", "read", "update", "delete"]]] = field( + default=None + ) @dataclass class TokenAttributes: + # pylint: disable=unsubscriptable-object """ TokenAttributes describes the set of input parameters that the POST /tokens API accepts. @@ -66,13 +95,15 @@ class TokenAttributes: # Name for the token. Required. name: str # The token's dataset-level capabilities. Keyed on dataset name. Optional. - datasetCapabilities: dict[str, TokenDatasetCapabilities] | None = None + datasetCapabilities: Optional[dict[str, TokenDatasetCapabilities]] = field( + default=None + ) # Description for the API token. Optional. - description: str | None = None + description: Optional[str] = field(default=None) # Expiration date for the API token. Optional. - expiresAt: str | None = None + expiresAt: Optional[str] = field(default=None) # The token's organization-level capabilities. Optional. - orgCapabilities: TokenOrganizationCapabilities | None = None + orgCapabilities: Optional[TokenOrganizationCapabilities] = field(default=None) @dataclass @@ -81,5 +112,6 @@ class Token: Token contains the response from a call to POST /tokens. It includes the API token itself, and an ID which can be used to reference it later. """ + id: str token: str diff --git a/pyproject.toml b/pyproject.toml index 8c5c540..e1000fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "axiom-py" -version = "0.4.0" +version = "0.5.0" description = "Axiom API Python bindings." authors = ["Axiom, Inc."] license = "MIT" @@ -22,10 +22,10 @@ rfc3339 = "^6.2" iso8601 = ">=1.0.2,<3.0.0" [tool.poetry.dev-dependencies] -black = "^23.3.0" -pytest = "^7.3.2" -pylint = "^2.7.2" -responses = "^0.23.1" +black = "^24.4.2" +pytest = "^8.2.1" +pylint = "^3.2.1" +responses = "^0.25.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/test_annotations.py b/tests/test_annotations.py index ed23490..e658372 100644 --- a/tests/test_annotations.py +++ b/tests/test_annotations.py @@ -3,7 +3,7 @@ import os import unittest -from typing import List, Dict, Any, Optional +from typing import List, Dict, Optional from logging import getLogger from requests.exceptions import HTTPError from datetime import timedelta diff --git a/tests/test_client.py b/tests/test_client.py index d2869d7..42dfd96 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -23,7 +23,7 @@ WrongQueryKindException, DatasetCreateRequest, TokenAttributes, - TokenOrganizationCapabilities + TokenOrganizationCapabilities, ) from axiom.query import ( QueryLegacy, @@ -171,8 +171,6 @@ def test_step005_apl_query(self): opts = AplOptions( start_time=startTime, end_time=endTime, - no_cache=True, - save=False, format=AplResultFormat.Legacy, ) qr = self.client.query(apl, opts) @@ -228,9 +226,7 @@ def test_api_tokens(self): """Test creating and deleting an API token""" token_attrs = TokenAttributes( name=f"PytestToken-{uuid.uuid4()}", - orgCapabilities=TokenOrganizationCapabilities( - apiTokens=["read"] - ) + orgCapabilities=TokenOrganizationCapabilities(apiTokens=["read"]), ) token_values = self.client.create_api_token(token_attrs) diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 8375f61..c7c4c33 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -3,7 +3,7 @@ import os import unittest -from typing import List, Dict, Any +from typing import List, Dict from logging import getLogger from requests.exceptions import HTTPError from datetime import timedelta @@ -17,7 +17,7 @@ class TestDatasets(unittest.TestCase): dataset_name: str - events: List[Dict[str, Any]] + events: List[Dict[str, object]] client: Client events_time_format = "%d/%b/%Y:%H:%M:%S +0000"