├── tests ├── __init__.py ├── momento │ ├── __init__.py │ ├── aio │ │ └── __init__.py │ ├── auth │ │ └── __init__.py │ ├── config │ │ └── __init__.py │ ├── local │ │ ├── __init__.py │ │ ├── momento_local_middleware_args.py │ │ ├── momento_error_code_metadata.py │ │ ├── momento_local_metrics_collector.py │ │ ├── test_fixed_count_retry_strategy.py │ │ └── test_fixed_count_retry_strategy_async.py │ ├── auth_client │ │ └── __init__.py │ ├── cache_client │ │ ├── __init__.py │ │ └── test_init.py │ ├── internal │ │ ├── __init__.py │ │ ├── _utilities │ │ │ ├── __init__.py │ │ │ ├── test_momento_version.py │ │ │ └── test_time.py │ │ └── test_credential_provider.py │ ├── requests │ │ ├── __init__.py │ │ └── test_collection_ttl.py │ ├── responses │ │ ├── __init__.py │ │ ├── test_cache_get_response.py │ │ ├── test_cache_list_fetch_response.py │ │ ├── test_cache_set_fetch_response.py │ │ └── test_cache_dictionary_fetch_response.py │ └── topic_client │ │ ├── __init__.py │ │ ├── shared_behaviors.py │ │ └── shared_behaviors_async.py ├── asserts.py └── utils.py ├── src └── momento │ ├── py.typed │ ├── internal │ ├── __init__.py │ ├── aio │ │ ├── __init__.py │ │ └── _scs_token_client.py │ ├── synchronous │ │ ├── __init__.py │ │ ├── _utilities.py │ │ └── _scs_token_client.py │ ├── services.py │ └── _utilities │ │ ├── _client_type.py │ │ ├── _python_runtime_version.py │ │ ├── _time.py │ │ ├── __init__.py │ │ └── _channel_credentials.py │ ├── responses │ ├── auth │ │ ├── __init__.py │ │ └── generate_disposable_token.py │ ├── data │ │ ├── __init__.py │ │ ├── list │ │ │ ├── __init__.py │ │ │ ├── remove_value.py │ │ │ ├── push_back.py │ │ │ ├── push_front.py │ │ │ ├── concatenate_back.py │ │ │ ├── concatenate_front.py │ │ │ ├── length.py │ │ │ ├── pop_back.py │ │ │ ├── pop_front.py │ │ │ └── fetch.py │ │ ├── set │ │ │ ├── __init__.py │ │ │ ├── add_element.py │ │ │ ├── add_elements.py │ │ │ ├── remove_element.py │ │ │ ├── remove_elements.py │ │ │ └── fetch.py │ │ ├── scalar │ │ │ ├── __init__.py │ │ │ ├── set.py │ │ │ ├── delete.py │ │ │ ├── set_if_not_exists.py │ │ │ ├── get.py │ │ │ └── increment.py │ │ ├── dictionary │ │ │ ├── __init__.py │ │ │ ├── set_field.py │ │ │ ├── set_fields.py │ │ │ ├── remove_field.py │ │ │ ├── remove_fields.py │ │ │ ├── increment.py │ │ │ ├── get_field.py │ │ │ └── fetch.py │ │ └── sorted_set │ │ │ ├── __init__.py │ │ │ ├── put_element.py │ │ │ ├── put_elements.py │ │ │ ├── remove_element.py │ │ │ ├── remove_elements.py │ │ │ ├── increment_score.py │ │ │ ├── get_rank.py │ │ │ ├── get_score.py │ │ │ ├── fetch.py │ │ │ └── get_scores.py │ ├── control │ │ ├── __init__.py │ │ ├── cache │ │ │ ├── __init__.py │ │ │ ├── flush.py │ │ │ ├── delete.py │ │ │ ├── create.py │ │ │ └── list.py │ │ └── signing_key │ │ │ ├── __init__.py │ │ │ ├── revoke.py │ │ │ ├── create.py │ │ │ └── list.py │ ├── pubsub │ │ ├── __init__.py │ │ ├── publish.py │ │ └── subscription_item.py │ └── mixins.py │ ├── auth │ ├── access_control │ │ ├── __init__.py │ │ ├── disposable_token_scope.py │ │ └── permission_scope.py │ └── __init__.py │ ├── config │ ├── transport │ │ ├── __init__.py │ │ ├── topic_grpc_configuration.py │ │ └── grpc_configuration.py │ ├── middleware │ │ ├── aio │ │ │ ├── __init__.py │ │ │ ├── middleware_metadata.py │ │ │ └── middleware.py │ │ ├── synchronous │ │ │ ├── __init__.py │ │ │ ├── middleware_metadata.py │ │ │ └── middleware.py │ │ ├── __init__.py │ │ └── models.py │ ├── __init__.py │ ├── topic_configurations.py │ ├── topic_configuration.py │ └── auth_configuration.py │ ├── common_data │ └── __init__.py │ ├── requests │ ├── sort_order.py │ └── __init__.py │ ├── retry │ ├── eligibility_strategy.py │ ├── retryable_props.py │ ├── __init__.py │ ├── retry_strategy.py │ └── fixed_count_retry_strategy.py │ ├── utilities │ ├── __init__.py │ ├── shared_sync_asyncio.py │ └── expiration.py │ ├── __init__.py │ ├── errors │ ├── __init__.py │ └── error_details.py │ ├── typing.py │ └── logs.py ├── examples ├── py310 │ ├── __init__.py │ ├── doc-example-files │ │ └── README.md │ ├── readme.py │ ├── topic_publish.py │ ├── topic_publish_async.py │ ├── quickstart.py │ ├── patterns.py │ ├── topic_subscribe.py │ └── topic_subscribe_async.py ├── prepy310 │ ├── __init__.py │ ├── readme.py │ ├── topic_publish.py │ ├── topic_publish_async.py │ ├── quickstart.py │ ├── topic_subscribe.py │ └── topic_subscribe_async.py ├── example_utils │ ├── __init__.py │ └── example_logging.py ├── lambda │ ├── infrastructure │ │ ├── dependencies.zip │ │ ├── .eslintignore │ │ ├── .npmignore │ │ ├── .gitignore │ │ ├── .prettierrc.json │ │ ├── jest.config.js │ │ ├── tsconfig.json │ │ ├── bin │ │ │ └── infrastructure.ts │ │ ├── package.json │ │ ├── lib │ │ │ └── stack.ts │ │ ├── cdk.json │ │ └── .eslintrc.json │ ├── zip │ │ ├── requirements.txt │ │ ├── .gitignore │ │ ├── Makefile │ │ └── index.py │ └── docker │ │ ├── lambda │ │ ├── requirements.txt │ │ └── index.py │ │ └── Dockerfile ├── config │ └── prometheus.yml ├── grafana │ └── provisioning │ │ ├── datasources │ │ └── datasource.yml │ │ └── dashboards │ │ └── dashboards.yml ├── Makefile ├── pyproject.toml ├── README.ja.md ├── utils │ └── instrumentation.py ├── docker-compose.yaml └── observability.py ├── .release-please-manifest.json ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── dependency-review.yml │ ├── momento-local-tests.yml │ ├── on-push-to-main-branch.yml │ └── codeql.yml ├── release-please-config.json └── README.template.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/py310/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/prepy310/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/internal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/aio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/local/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/example_utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/internal/aio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/data/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/auth_client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/cache_client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/internal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/requests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/responses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/topic_client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/auth/access_control/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/config/transport/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/control/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/data/set/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/pubsub/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/dependencies.zip: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/internal/synchronous/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/control/cache/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/data/scalar/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/momento/internal/_utilities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/control/signing_key/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/lambda/zip/requirements.txt: -------------------------------------------------------------------------------- 1 | momento==1.20.1 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.28.0" 3 | } 4 | -------------------------------------------------------------------------------- /examples/lambda/zip/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | dist 3 | dist.zip 4 | -------------------------------------------------------------------------------- /examples/lambda/docker/lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | momento==1.8.0 2 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | **/*.d.ts 4 | -------------------------------------------------------------------------------- /src/momento/common_data/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for common data between request and response types.""" 2 | -------------------------------------------------------------------------------- /src/momento/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .credential_provider import CredentialProvider 2 | 3 | __all__ = ["CredentialProvider"] 4 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /src/momento/requests/sort_order.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class SortOrder(Enum): 5 | ASCENDING = True 6 | DESCENDING = False 7 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /examples/py310/doc-example-files/README.md: -------------------------------------------------------------------------------- 1 | This directory contains files that will be pulled in to the docs site for use in code examples. 2 | Use caution when modifying any files in this directory! 3 | -------------------------------------------------------------------------------- /tests/momento/internal/_utilities/test_momento_version.py: -------------------------------------------------------------------------------- 1 | from momento import __version__ as momento_version 2 | 3 | 4 | def test_momento_version() -> None: 5 | assert momento_version != "" 6 | -------------------------------------------------------------------------------- /src/momento/requests/__init__.py: -------------------------------------------------------------------------------- 1 | """Moment client request types.""" 2 | 3 | from .collection_ttl import CollectionTtl 4 | from .sort_order import SortOrder 5 | 6 | __all__ = ["CollectionTtl", "SortOrder"] 7 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "arrowParens": "avoid", 6 | "printWidth": 120 7 | } 8 | 9 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /examples/config/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s 3 | evaluation_interval: 15s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: ['host.docker.internal:9464'] 9 | 10 | -------------------------------------------------------------------------------- /examples/grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | url: http://prometheus:9090 7 | isDefault: true 8 | access: proxy 9 | editable: true 10 | -------------------------------------------------------------------------------- /src/momento/internal/services.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Service(Enum): 5 | CACHE = "cache" 6 | TOPICS = "topics" 7 | INDEX = "index" 8 | AUTH = "auth" # not really a service we provide, but we have APIs for auth that applies to all services 9 | -------------------------------------------------------------------------------- /src/momento/retry/eligibility_strategy.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from .retryable_props import RetryableProps 4 | 5 | 6 | class EligibilityStrategy(ABC): 7 | @abstractmethod 8 | def is_eligible_for_retry(self, props: RetryableProps) -> bool: 9 | pass 10 | -------------------------------------------------------------------------------- /src/momento/config/middleware/aio/__init__.py: -------------------------------------------------------------------------------- 1 | from momento.config.middleware.aio.middleware import Middleware, MiddlewareRequestHandler 2 | from momento.config.middleware.aio.middleware_metadata import MiddlewareMetadata 3 | 4 | __all__ = ["Middleware", "MiddlewareMetadata", "MiddlewareRequestHandler"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules. 2 | **/*.pyc 3 | **/__pycache__ 4 | 5 | # Setuptools distribution folder. 6 | dist/ 7 | 8 | # Don't checkin the pyenv/tox files 9 | .tox 10 | .python-version 11 | 12 | .idea 13 | .vscode 14 | 15 | momento-examples-env/ 16 | 17 | *.egg-info 18 | build 19 | certs 20 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: export 2 | export: 3 | @poetry export -f requirements.txt --output requirements.txt 4 | 5 | .PHONY: format 6 | format: 7 | @poetry run ruff format prepy310 py310 8 | 9 | .PHONY: lint 10 | lint: 11 | @poetry run mypy prepy310 py310 12 | @poetry run ruff check prepy310 py310 13 | -------------------------------------------------------------------------------- /src/momento/config/middleware/aio/middleware_metadata.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from grpc.aio import Metadata 5 | 6 | 7 | @dataclass 8 | class MiddlewareMetadata: 9 | """Wrapper for gRPC metadata.""" 10 | 11 | grpc_metadata: Optional[Metadata] 12 | -------------------------------------------------------------------------------- /src/momento/config/middleware/synchronous/__init__.py: -------------------------------------------------------------------------------- 1 | from momento.config.middleware.synchronous.middleware import Middleware, MiddlewareRequestHandler 2 | from momento.config.middleware.synchronous.middleware_metadata import MiddlewareMetadata 3 | 4 | __all__ = ["Middleware", "MiddlewareMetadata", "MiddlewareRequestHandler"] 5 | -------------------------------------------------------------------------------- /src/momento/config/middleware/synchronous/middleware_metadata.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from grpc._typing import MetadataType 5 | 6 | 7 | @dataclass 8 | class MiddlewareMetadata: 9 | """Wrapper for gRPC metadata.""" 10 | 11 | grpc_metadata: Optional[MetadataType] 12 | -------------------------------------------------------------------------------- /src/momento/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | from .expiration import Expiration, ExpiresAt, ExpiresIn 2 | from .shared_sync_asyncio import DEFAULT_EAGER_CONNECTION_TIMEOUT_SECONDS, str_to_bytes 3 | 4 | __all__ = [ 5 | "Expiration", 6 | "ExpiresAt", 7 | "ExpiresIn", 8 | "DEFAULT_EAGER_CONNECTION_TIMEOUT_SECONDS", 9 | "str_to_bytes", 10 | ] 11 | -------------------------------------------------------------------------------- /src/momento/internal/_utilities/_client_type.py: -------------------------------------------------------------------------------- 1 | """Enumerates the types of clients that can be used. 2 | 3 | Used to populate the agent header in gRPC requests. 4 | """ 5 | 6 | from enum import Enum 7 | 8 | 9 | class ClientType(Enum): 10 | """Describes the type of client that is being used.""" 11 | 12 | CACHE = "cache" 13 | TOPIC = "topic" 14 | TOKEN = "token" 15 | -------------------------------------------------------------------------------- /src/momento/utilities/shared_sync_asyncio.py: -------------------------------------------------------------------------------- 1 | DEFAULT_EAGER_CONNECTION_TIMEOUT_SECONDS = 30 2 | 3 | 4 | def str_to_bytes(string: str) -> bytes: 5 | """Convert a string to bytes. 6 | 7 | Args: 8 | string (str): The string to convert. 9 | 10 | Returns: 11 | bytes: A UTF-8 byte representation of the string. 12 | """ 13 | return string.encode("utf-8") 14 | -------------------------------------------------------------------------------- /src/momento/internal/_utilities/_python_runtime_version.py: -------------------------------------------------------------------------------- 1 | """Python runtime version information. 2 | 3 | Used to populate the `runtime-version` header in gRPC requests. 4 | """ 5 | import sys 6 | 7 | PYTHON_RUNTIME_VERSION = ( 8 | f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} " 9 | f"({sys.version_info.releaselevel} {sys.version_info.serial})" 10 | ) 11 | -------------------------------------------------------------------------------- /src/momento/config/__init__.py: -------------------------------------------------------------------------------- 1 | """Momento network configuration module.""" 2 | 3 | from .configuration import Configuration 4 | from .configurations import Configurations 5 | from .topic_configuration import TopicConfiguration 6 | from .topic_configurations import TopicConfigurations 7 | 8 | __all__ = [ 9 | "Configuration", 10 | "Configurations", 11 | "TopicConfiguration", 12 | "TopicConfigurations", 13 | ] 14 | -------------------------------------------------------------------------------- /tests/momento/internal/test_credential_provider.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from momento.auth import CredentialProvider 4 | 5 | 6 | def describe_credential_provider() -> None: 7 | def it_obscures_the_auth_token(bad_token_credential_provider: CredentialProvider) -> None: 8 | cp = bad_token_credential_provider 9 | assert not re.search(r"{cp.auth_token}", cp.__repr__()) 10 | assert not re.search(r"{cp.auth_token}", str(cp)) 11 | -------------------------------------------------------------------------------- /src/momento/internal/_utilities/_time.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | 4 | def _timedelta_to_ms(delta: timedelta) -> int: 5 | """Expresses a timedelta as milliseconds. 6 | 7 | Note: truncates the microseconds. 8 | 9 | Args: 10 | delta (timedelta): The timedelta to convert. 11 | 12 | Returns: 13 | int: The total duration of the timedelta in milliseconds. 14 | """ 15 | return int(delta.total_seconds() * 1000) 16 | -------------------------------------------------------------------------------- /examples/lambda/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 public.ecr.aws/lambda/python:3.8 2 | 3 | WORKDIR /var/task 4 | 5 | # Copy the lambda and the requirements file 6 | COPY lambda/index.py . 7 | COPY lambda/requirements.txt . 8 | 9 | # Install Python dependencies 10 | RUN pip install -r requirements.txt -t . 11 | 12 | # Set the CMD to your lambda (could also be done as a parameter override outside of the Dockerfile) 13 | CMD ["index.handler"] 14 | 15 | -------------------------------------------------------------------------------- /src/momento/retry/retryable_props.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | import grpc 6 | 7 | 8 | # The lack of type hints in the grpc code causes a lint failure in combination with the dataclass annotation 9 | @dataclass # type: ignore[misc] 10 | class RetryableProps: 11 | grpc_status: grpc.StatusCode 12 | grpc_method: str 13 | attempt_number: int 14 | overall_deadline: Optional[datetime] = None 15 | -------------------------------------------------------------------------------- /src/momento/retry/__init__.py: -------------------------------------------------------------------------------- 1 | from .default_eligibility_strategy import DefaultEligibilityStrategy 2 | from .eligibility_strategy import EligibilityStrategy 3 | from .fixed_count_retry_strategy import FixedCountRetryStrategy 4 | from .retry_strategy import RetryStrategy 5 | from .retryable_props import RetryableProps 6 | 7 | __all__ = [ 8 | "DefaultEligibilityStrategy", 9 | "EligibilityStrategy", 10 | "FixedCountRetryStrategy", 11 | "RetryStrategy", 12 | "RetryableProps", 13 | ] 14 | -------------------------------------------------------------------------------- /src/momento/retry/retry_strategy.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from datetime import datetime 3 | from typing import Optional 4 | 5 | from .retryable_props import RetryableProps 6 | 7 | 8 | class RetryStrategy(ABC): 9 | @abstractmethod 10 | def determine_when_to_retry(self, props: RetryableProps) -> Optional[float]: 11 | pass 12 | 13 | # Currently used only by the FixedTimeoutRetryStrategy 14 | def calculate_retry_deadline(self, overall_deadline: datetime) -> Optional[float]: 15 | return None 16 | -------------------------------------------------------------------------------- /src/momento/config/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from momento.config.middleware.aio import Middleware as AsyncMiddleware 4 | from momento.config.middleware.models import ( 5 | MiddlewareMessage, 6 | MiddlewareRequestHandlerContext, 7 | MiddlewareStatus, 8 | ) 9 | from momento.config.middleware.synchronous import Middleware as SyncMiddleware 10 | 11 | Middleware = Union[SyncMiddleware, AsyncMiddleware] 12 | 13 | __all__ = [ 14 | "Middleware", 15 | "MiddlewareMessage", 16 | "MiddlewareStatus", 17 | "MiddlewareRequestHandlerContext", 18 | ] 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/examples" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | allow: 13 | - dependency-name: "momento" 14 | -------------------------------------------------------------------------------- /tests/momento/responses/test_cache_get_response.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from momento.responses import CacheGet 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "value_bytes, expected_str", 9 | [ 10 | (b"hello", "CacheGet.Hit(value_bytes=b'hello')"), 11 | (("i" * 100).encode(), "CacheGet.Hit(value_bytes=b'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii...')"), 12 | ], 13 | ) 14 | def test_dictionary_fetch_hit_str_and_repr(value_bytes: bytes, expected_str: str) -> None: 15 | hit = CacheGet.Hit(value_bytes) 16 | assert str(hit) == expected_str 17 | assert eval(repr(hit)) == hit 18 | -------------------------------------------------------------------------------- /examples/prepy310/readme.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from momento import CacheClient, Configurations, CredentialProvider 4 | from momento.responses import CacheGet 5 | 6 | cache_client = CacheClient( 7 | configuration=Configurations.Laptop.v1(), 8 | credential_provider=CredentialProvider.from_environment_variable("MOMENTO_API_KEY"), 9 | default_ttl=timedelta(seconds=60), 10 | ) 11 | cache_client.create_cache("cache") 12 | cache_client.set("cache", "myKey", "myValue") 13 | get_response = cache_client.get("cache", "myKey") 14 | if isinstance(get_response, CacheGet.Hit): 15 | print(f"Got value: {get_response.value_string}") 16 | -------------------------------------------------------------------------------- /examples/py310/readme.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from momento import CacheClient, Configurations, CredentialProvider 4 | from momento.responses import CacheGet 5 | 6 | cache_client = CacheClient( 7 | Configurations.Laptop.v1(), CredentialProvider.from_environment_variable("MOMENTO_API_KEY"), timedelta(seconds=60) 8 | ) 9 | 10 | cache_client.create_cache("cache") 11 | cache_client.set("cache", "my-key", "my-value") 12 | get_response = cache_client.get("cache", "my-key") 13 | match get_response: 14 | case CacheGet.Hit() as hit: 15 | print(f"Got value: {hit.value_string}") 16 | case _: 17 | print(f"Response was not a hit: {get_response}") 18 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "packages": { 4 | ".": { 5 | "release-type": "python", 6 | "changelog-sections": [ 7 | { 8 | "type": "feat", 9 | "section": "Features", 10 | "hidden": false 11 | }, 12 | { 13 | "type": "fix", 14 | "section": "Bug Fixes", 15 | "hidden": false 16 | }, 17 | { 18 | "type": "chore", 19 | "section": "Miscellaneous", 20 | "hidden": false 21 | } 22 | ], 23 | "extra-files": ["src/momento/__init__.py"] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/momento/internal/_utilities/__init__.py: -------------------------------------------------------------------------------- 1 | from ._client_type import ClientType 2 | from ._data_validation import ( 3 | _as_bytes, 4 | _gen_dictionary_fields_as_bytes, 5 | _gen_dictionary_items_as_bytes, 6 | _gen_list_as_bytes, 7 | _gen_set_input_as_bytes, 8 | _validate_cache_name, 9 | _validate_dictionary_name, 10 | _validate_disposable_token_expiry, 11 | _validate_eager_connection_timeout, 12 | _validate_list_name, 13 | _validate_request_timeout, 14 | _validate_set_name, 15 | _validate_timedelta_ttl, 16 | _validate_topic_name, 17 | _validate_ttl, 18 | ) 19 | from ._python_runtime_version import PYTHON_RUNTIME_VERSION 20 | from ._time import _timedelta_to_ms 21 | -------------------------------------------------------------------------------- /examples/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "examples" 3 | version = "0.1.0" 4 | description = "Momento Python SDK Examples for Python" 5 | authors = ["Momento "] 6 | license = "Apache-2.0" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3.7,<3.13" 10 | 11 | momento = "1.27.0" 12 | colorlog = "6.7.0" 13 | hdrhistogram = "^0.10.1" 14 | 15 | [tool.poetry.group.lint.dependencies] 16 | mypy = "^0.971" 17 | ruff = "^0.1.6" 18 | 19 | [tool.mypy] 20 | [[tool.mypy.overrides]] 21 | module = ["momento.*", "", "example_utils.*"] 22 | ignore_missing_imports = true 23 | 24 | [tool.ruff] 25 | line-length = 120 26 | fix = true 27 | 28 | [build-system] 29 | requires = ["poetry-core"] 30 | build-backend = "poetry.core.masonry.api" 31 | -------------------------------------------------------------------------------- /tests/asserts.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from momento.errors.error_details import MomentoErrorCode 4 | from momento.responses.mixins import ErrorResponseMixin 5 | from momento.responses.response import Response 6 | 7 | 8 | # Custom assertions 9 | def assert_response_is_error( 10 | response: Response, 11 | *, 12 | error_code: Optional[MomentoErrorCode] = None, 13 | inner_exception_message: Optional[str] = None, 14 | ) -> None: 15 | assert isinstance(response, ErrorResponseMixin) 16 | if isinstance(response, ErrorResponseMixin): 17 | if error_code: 18 | assert response.error_code == error_code 19 | if inner_exception_message: 20 | assert response.inner_exception.message == inner_exception_message 21 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2021", 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ], 25 | "outDir": "./dist" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/momento/internal/_utilities/test_time.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | import pytest 4 | from momento.internal._utilities import _timedelta_to_ms 5 | 6 | 7 | @pytest.mark.parametrize( 8 | ["delta", "actual_ms"], 9 | [ 10 | [timedelta(days=1), 24 * 60 * 60 * 1000], 11 | [timedelta(hours=1), 60 * 60 * 1000], 12 | [timedelta(minutes=1), 60 * 1000], 13 | [timedelta(seconds=1), 1000], 14 | [timedelta(milliseconds=1), 1], 15 | [timedelta(microseconds=1), 0], 16 | [timedelta(days=1, seconds=1), 24 * 60 * 60 * 1000 + 1000], 17 | [timedelta(seconds=1, milliseconds=100), 1100], 18 | [timedelta(milliseconds=1100), 1100], 19 | ], 20 | ) 21 | def test_timedelta_to_ms(delta: timedelta, actual_ms: int) -> None: 22 | assert _timedelta_to_ms(delta) == actual_ms 23 | -------------------------------------------------------------------------------- /src/momento/responses/pubsub/publish.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ..mixins import ErrorResponseMixin 4 | from ..response import PubsubResponse 5 | 6 | 7 | class TopicPublishResponse(PubsubResponse): 8 | """Parent response type for a topic `publish` request. 9 | 10 | Its subtypes are: 11 | - `TopicPublish.Success` 12 | - `TopicPublish.Error` 13 | """ 14 | 15 | 16 | class TopicPublish(ABC): 17 | """Groups all `TopicPublishResponse` derived types under a common namespace.""" 18 | 19 | class Success(TopicPublishResponse): 20 | """Indicates the request was successful.""" 21 | 22 | class Error(TopicPublishResponse, ErrorResponseMixin): 23 | """Contains information about an error returned from a request. 24 | 25 | This includes: 26 | - `error_code`: `MomentoErrorCode` value for the error. 27 | - `message`: a detailed error message. 28 | """ 29 | -------------------------------------------------------------------------------- /tests/momento/responses/test_cache_list_fetch_response.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from momento.responses import CacheListFetch 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "list_, expected_str", 9 | [ 10 | ( 11 | [b"hello", b"world"], 12 | "CacheListFetch.Hit(value_list_bytes=[b'hello', b'world'])", 13 | ), 14 | ( 15 | [("i" * 100).encode()], 16 | f"CacheListFetch.Hit(value_list_bytes=[b'{'i'*32}...'])", 17 | ), 18 | ( 19 | [f"{i}".encode() for i in range(10)], 20 | "CacheListFetch.Hit(value_list_bytes=[b'0', b'1', b'2', b'3', b'4', ...])", 21 | ), 22 | ], 23 | ) 24 | def test_list_fetch_hit_str_and_repr(list_: list[bytes], expected_str: str) -> None: 25 | hit = CacheListFetch.Hit(list_) 26 | assert str(hit) == expected_str 27 | assert eval(repr(hit)) == hit 28 | -------------------------------------------------------------------------------- /tests/momento/responses/test_cache_set_fetch_response.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from momento.responses import CacheSetFetch 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "set_, expected_str", 9 | [ 10 | ({b"hello"}, "CacheSetFetch.Hit(value_set_bytes={b'hello'})"), 11 | ( 12 | {("i" * 100).encode()}, 13 | f"CacheSetFetch.Hit(values_bytes=[b'{'i'*32}...'])", 14 | ), 15 | ( 16 | {f"{i}".encode() for i in range(10)}, 17 | "CacheSetFetch.Hit(values_bytes=[b'0', b'1', b'2', b'3', b'4', ...])", 18 | ), 19 | ], 20 | ) 21 | def test_set_fetch_hit_str_and_repr(set_: set[bytes], expected_str: str) -> None: 22 | hit = CacheSetFetch.Hit(set_) 23 | if "..." in expected_str: 24 | assert "..." in str(hit) 25 | else: 26 | assert str(hit) == expected_str 27 | assert eval(repr(hit)) == hit 28 | -------------------------------------------------------------------------------- /src/momento/internal/_utilities/_channel_credentials.py: -------------------------------------------------------------------------------- 1 | """Helper functions for creating gRPC channel credentials. 2 | 3 | Note that this isn't in the _utilities init to avoid a circular import. 4 | """ 5 | from __future__ import annotations 6 | 7 | import grpc 8 | 9 | from momento.config import Configuration 10 | 11 | 12 | def channel_credentials_from_root_certs_or_default( 13 | config: Configuration, 14 | ) -> grpc.ChannelCredentials: 15 | """Create gRPC channel credentials from the root certificates or the default credentials. 16 | 17 | Args: 18 | config (Configuration): the configuration to use. 19 | 20 | Returns: 21 | grpc.ChannelCredentials: the gRPC channel credentials. 22 | """ 23 | root_certificates = config.get_transport_strategy().get_grpc_configuration().get_root_certificates_pem() 24 | return grpc.ssl_channel_credentials(root_certificates=root_certificates) # type: ignore[misc] 25 | -------------------------------------------------------------------------------- /examples/example_utils/example_logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import colorlog # type: ignore 5 | from momento.logs import TRACE, initialize_momento_logging 6 | 7 | 8 | def initialize_logging() -> None: 9 | initialize_momento_logging() 10 | debug_mode = os.getenv("DEBUG") 11 | trace_mode = os.getenv("TRACE") 12 | if trace_mode == "true": 13 | log_level = TRACE 14 | elif debug_mode == "true": 15 | log_level = logging.DEBUG 16 | else: 17 | log_level = logging.INFO 18 | 19 | root_logger = logging.getLogger() 20 | root_logger.setLevel(log_level) 21 | 22 | handler = colorlog.StreamHandler() 23 | handler.setFormatter( 24 | colorlog.ColoredFormatter( 25 | "%(asctime)s %(log_color)s%(levelname)-8s%(reset)s %(thin_cyan)s%(name)s%(reset)s %(message)s" 26 | ) 27 | ) 28 | handler.setLevel(log_level) 29 | root_logger.addHandler(handler) 30 | -------------------------------------------------------------------------------- /src/momento/responses/data/scalar/set.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheSetResponse(CacheResponse): 8 | """Parent response type for a cache `set` request. 9 | 10 | Its subtypes are: 11 | - `CacheSet.Success` 12 | - `CacheSet.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheSet(ABC): 19 | """Groups all `CacheSetResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheSetResponse): 22 | """Indicates the request was successful.""" 23 | 24 | class Error(CacheSetResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /src/momento/responses/data/scalar/delete.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheDeleteResponse(CacheResponse): 8 | """Parent response type for a cache `delete` request. 9 | 10 | Its subtypes are: 11 | - `CacheDelete.Success` 12 | - `CacheDelete.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheDelete(ABC): 19 | """Groups all `CacheDeleteResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheDeleteResponse): 22 | """Indicates the request was successful.""" 23 | 24 | class Error(CacheDeleteResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v2 21 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/bin/infrastructure.ts: -------------------------------------------------------------------------------- 1 | import { App } from "aws-cdk-lib"; 2 | import {MomentoLambdaStack} from "../lib/stack"; 3 | 4 | const app = new App(); 5 | new MomentoLambdaStack(app, 'MomentoPythonLambda', { 6 | /* If you don't specify 'env', this stack will be environment-agnostic. 7 | * Account/Region-dependent features and context lookups will not work, 8 | * but a single synthesized template can be deployed anywhere. */ 9 | /* Uncomment the next line to specialize this stack for the AWS Account 10 | * and Region that are implied by the current CLI configuration. */ 11 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 12 | /* Uncomment the next line if you know exactly what Account and Region you 13 | * want to deploy the stack to. */ 14 | // env: { account: '123456789012', region: 'us-east-1' }, 15 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 16 | }); 17 | -------------------------------------------------------------------------------- /src/momento/config/middleware/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict 3 | 4 | import grpc 5 | from google.protobuf.message import Message 6 | 7 | CONNECTION_ID_KEY = "connectionID" 8 | 9 | 10 | @dataclass 11 | class MiddlewareMessage: 12 | """Wrapper for a gRPC protobuf message.""" 13 | 14 | grpc_message: Message 15 | 16 | @property 17 | def message_length(self) -> int: 18 | """Length of the message in bytes.""" 19 | return len(self.grpc_message.SerializeToString()) 20 | 21 | @property 22 | def constructor_name(self) -> str: 23 | """The class name of the message.""" 24 | return str(self.grpc_message.__class__.__name__) 25 | 26 | 27 | @dataclass 28 | class MiddlewareStatus: 29 | """Wrapper for gRPC status.""" 30 | 31 | grpc_status: grpc.StatusCode 32 | 33 | 34 | @dataclass 35 | class MiddlewareRequestHandlerContext: 36 | """Context for middleware request handlers.""" 37 | 38 | context: Dict[str, str] 39 | -------------------------------------------------------------------------------- /src/momento/responses/control/cache/flush.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from momento.responses.mixins import ErrorResponseMixin 4 | from momento.responses.response import ControlResponse 5 | 6 | 7 | class CacheFlushResponse(ControlResponse): 8 | """Parent response type for a cache `flush` request. 9 | 10 | Its subtypes are: 11 | - `CacheFlush.Success` 12 | - `CacheFlush.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheFlush(ABC): 19 | """Groups all `CacheFlushResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheFlushResponse): 22 | """Indicates the request was successful.""" 23 | 24 | class Error(CacheFlushResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /examples/grafana/provisioning/dashboards/dashboards.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | # an unique provider name. Required 5 | - name: 'prometheus' 6 | # Org id. Default to 1 7 | # orgId: 1 8 | # name of the dashboard folder. 9 | # folder: '' 10 | # folder UID. will be automatically generated if not specified 11 | # folderUid: '' 12 | # provider type. Default to 'file' 13 | type: file 14 | # disable dashboard deletion 15 | disableDeletion: false 16 | # how often Grafana will scan for changed dashboards 17 | updateIntervalSeconds: 10 18 | # allow updating provisioned dashboards from the UI 19 | allowUiUpdates: true 20 | options: 21 | # path to dashboard files on disk. Required when using the 'file' type 22 | path: /var/lib/grafana/dashboards 23 | # use folder names from filesystem to create folders in Grafana 24 | foldersFromFilesStructure: false 25 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import time 5 | import uuid 6 | 7 | 8 | def unique_test_cache_name() -> str: 9 | return f"python-test-{uuid_str()}" 10 | 11 | 12 | def uuid_str() -> str: 13 | """Generate a UUID as a string. 14 | 15 | Returns: 16 | str: A UUID 17 | """ 18 | return str(uuid.uuid4()) 19 | 20 | 21 | def uuid_bytes() -> bytes: 22 | """Generate a UUID as bytes. 23 | 24 | Returns: 25 | bytes: A UUID 26 | """ 27 | return uuid.uuid4().bytes 28 | 29 | 30 | def str_to_bytes(string: str) -> bytes: 31 | """Convert a string to bytes. 32 | 33 | Args: 34 | string (str): The string to convert. 35 | 36 | Returns: 37 | bytes: A UTF-8 byte representation of the string. 38 | """ 39 | return string.encode("utf-8") 40 | 41 | 42 | def sleep(seconds: int) -> None: 43 | time.sleep(seconds) 44 | 45 | 46 | async def sleep_async(seconds: int) -> None: 47 | await asyncio.sleep(seconds) 48 | -------------------------------------------------------------------------------- /src/momento/config/transport/topic_grpc_configuration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from datetime import timedelta 5 | from typing import Optional 6 | 7 | 8 | class TopicGrpcConfiguration(ABC): 9 | @abstractmethod 10 | def get_deadline(self) -> timedelta: 11 | pass 12 | 13 | @abstractmethod 14 | def with_deadline(self, deadline: timedelta) -> TopicGrpcConfiguration: 15 | pass 16 | 17 | @abstractmethod 18 | def get_max_send_message_length(self) -> Optional[int]: 19 | pass 20 | 21 | @abstractmethod 22 | def get_max_receive_message_length(self) -> Optional[int]: 23 | pass 24 | 25 | @abstractmethod 26 | def get_keepalive_permit_without_calls(self) -> Optional[int]: 27 | pass 28 | 29 | @abstractmethod 30 | def get_keepalive_time(self) -> Optional[timedelta]: 31 | pass 32 | 33 | @abstractmethod 34 | def get_keepalive_timeout(self) -> Optional[timedelta]: 35 | pass 36 | -------------------------------------------------------------------------------- /src/momento/responses/data/set/add_element.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheSetAddElementResponse(CacheResponse): 8 | """Parent response type for a `set_add_element` request. 9 | 10 | Its subtypes are: 11 | - `CacheSetAddElement.Success` 12 | - `CacheSetAddElement.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheSetAddElement(ABC): 19 | """Groups all `CacheSetAddElementResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheSetAddElementResponse): 22 | """Indicates the element was added.""" 23 | 24 | class Error(CacheSetAddElementResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/remove_value.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheListRemoveValueResponse(CacheResponse): 8 | """Response type for a `list_remove_value` request. 9 | 10 | Its subtypes are: 11 | - `CacheListRemoveValue.Success` 12 | - `CacheListRemoveValue.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheListRemoveValue(ABC): 19 | """Groups all `CacheListRemoveValueResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheListRemoveValueResponse): 22 | """Indicates removing the values was successful.""" 23 | 24 | class Error(CacheListRemoveValueResponse, ErrorResponseMixin): 25 | """Indicates an error occured in the request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /src/momento/responses/data/set/add_elements.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheSetAddElementsResponse(CacheResponse): 8 | """Parent response type for a `set_add_elements` request. 9 | 10 | Its subtypes are: 11 | - `CacheSetAddElements.Success` 12 | - `CacheSetAddElements.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheSetAddElements(ABC): 19 | """Groups all `CacheSetAddElementsResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheSetAddElementsResponse): 22 | """Indicates the elements were added.""" 23 | 24 | class Error(CacheSetAddElementsResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /src/momento/responses/data/set/remove_element.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheSetRemoveElementResponse(CacheResponse): 8 | """Parent response type for a `set_remove_element` request. 9 | 10 | Its subtypes are: 11 | - `CacheSetRemoveElement.Success` 12 | - `CacheSetRemoveElement.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheSetRemoveElement(ABC): 19 | """Groups all `CacheSetRemoveElementResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheSetRemoveElementResponse): 22 | """Indicates the element was removed.""" 23 | 24 | class Error(CacheSetRemoveElementResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /examples/README.ja.md: -------------------------------------------------------------------------------- 1 | # Python クライアント SDK 2 | 3 | _他言語バージョンもあります_:[English](README.md) 4 | 5 |
6 | 7 | ## SDK コード例を実行する 8 | 9 | - [Python 3.7 もしくはそれ以上が必要です。](https://www.python.org/downloads/) 10 | - Momento オーストークンが必要です。トークン発行は[Momento CLI](https://github.com/momentohq/momento-cli)から行えます。 11 | 12 | ```bash 13 | python3 -m pip install --user pipenv 14 | pipenv install 15 | ``` 16 | 17 | ```bash 18 | MOMENTO_API_KEY= pipenv run python example.py 19 | MOMENTO_API_KEY= pipenv run python example_async.py 20 | ``` 21 | 22 | SDK のデバッグログをオンするには、下記のように実行して下さい: 23 | 24 | ```bash 25 | DEBUG=true MOMENTO_API_KEY= pipenv run python example.py 26 | DEBUG=true MOMENTO_API_KEY= pipenv run python example_async.py 27 | ``` 28 | 29 | ## SDK を自身のプロジェクトで使用する 30 | 31 | ```bash 32 | pipenv install momento==0.14.0 33 | ``` 34 | 35 | or 36 | 37 | `momento==0.14.0`を`requirements.txt`に追加する、もしくは自身のプロジェクで使用しているディペンデンシー管理フレームワークに追加して下さい。 38 | 39 | 自身のシステムに直接インストールする方法: 40 | 41 | ```bash 42 | pip install momento==0.14.0 43 | ``` 44 | -------------------------------------------------------------------------------- /tests/momento/local/momento_local_middleware_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Optional 3 | 4 | from momento.errors import MomentoErrorCode 5 | 6 | from tests.momento.local.momento_local_metrics_collector import MomentoLocalMetricsCollector 7 | from tests.momento.local.momento_rpc_method import MomentoRpcMethod 8 | 9 | 10 | @dataclass 11 | class MomentoLocalMiddlewareArgs: 12 | """Arguments for Momento local middleware.""" 13 | 14 | request_id: str 15 | test_metrics_collector: Optional[MomentoLocalMetricsCollector] = None 16 | return_error: Optional[MomentoErrorCode] = None 17 | error_rpc_list: Optional[List[MomentoRpcMethod]] = None 18 | error_count: Optional[int] = None 19 | delay_rpc_list: Optional[List[MomentoRpcMethod]] = None 20 | delay_millis: Optional[int] = None 21 | delay_count: Optional[int] = None 22 | stream_error_rpc_list: Optional[List[MomentoRpcMethod]] = None 23 | stream_error: Optional[MomentoErrorCode] = None 24 | stream_error_message_limit: Optional[int] = None 25 | -------------------------------------------------------------------------------- /src/momento/responses/data/set/remove_elements.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheSetRemoveElementsResponse(CacheResponse): 8 | """Parent response type for a `set_remove_elements` request. 9 | 10 | Its subtypes are: 11 | - `CacheSetRemoveElements.Success` 12 | - `CacheSetRemoveElements.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheSetRemoveElements(ABC): 19 | """Groups all `CacheSetRemoveElementsResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheSetRemoveElementsResponse): 22 | """Indicates the elements were removed.""" 23 | 24 | class Error(CacheSetRemoveElementsResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/set_field.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheDictionarySetFieldResponse(CacheResponse): 8 | """Parent response type for a cache `dictionary_set_field` request. 9 | 10 | Its subtypes are: 11 | - `CacheDictionarySetField.Success` 12 | - `CacheDictionarySetField.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheDictionarySetField(ABC): 19 | """Groups all `CacheDictionarySetFieldResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheDictionarySetFieldResponse): 22 | """Indicates the request was successful.""" 23 | 24 | class Error(CacheDictionarySetFieldResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/set_fields.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheDictionarySetFieldsResponse(CacheResponse): 8 | """Parent response type for a cache `dictionary_set_fields` request. 9 | 10 | Its subtypes are: 11 | - `CacheDictionarySetFields.Success` 12 | - `CacheDictionarySetFields.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheDictionarySetFields(ABC): 19 | """Groups all `CacheDictionarySetFieldsResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheDictionarySetFieldsResponse): 22 | """Indicates the request was successful.""" 23 | 24 | class Error(CacheDictionarySetFieldsResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infrastructure", 3 | "version": "0.1.0", 4 | "bin": { 5 | "infrastructure": "bin/infrastructure.js" 6 | }, 7 | "scripts": { 8 | "prebuild": "eslint . --ext .ts", 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest", 12 | "cdk": "cdk", 13 | "lint": "eslint . --ext .ts", 14 | "format": "eslint . --ext .ts --fix" 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^29.4.0", 18 | "@types/node": "18.11.18", 19 | "@typescript-eslint/eslint-plugin": "5.59.11", 20 | "aws-cdk": "2.85.0", 21 | "eslint": "8.42.0", 22 | "eslint-config-prettier": "8.8.0", 23 | "eslint-plugin-import": "2.27.5", 24 | "eslint-plugin-node": "11.1.0", 25 | "eslint-plugin-prettier": "4.2.1", 26 | "jest": "^29.4.1", 27 | "prettier": "2.8.8", 28 | "ts-jest": "^29.0.5", 29 | "ts-node": "^10.9.1", 30 | "typescript": "~4.9.4" 31 | }, 32 | "dependencies": { 33 | "aws-cdk-lib": "^2.85.0", 34 | "constructs": "^10.0.0", 35 | "source-map-support": "^0.5.21" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/remove_field.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheDictionaryRemoveFieldResponse(CacheResponse): 8 | """Parent response type for a cache `dictionary_remove_field` request. 9 | 10 | Its subtypes are: 11 | - `CacheDictionaryRemoveField.Success` 12 | - `CacheDictionaryRemoveField.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheDictionaryRemoveField(ABC): 19 | """Groups all `CacheDictionaryRemoveFieldResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheDictionaryRemoveFieldResponse): 22 | """Indicates the request was successful.""" 23 | 24 | class Error(CacheDictionaryRemoveFieldResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /examples/utils/instrumentation.py: -------------------------------------------------------------------------------- 1 | from opentelemetry import trace 2 | from opentelemetry.exporter.zipkin.json import ZipkinExporter 3 | from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient 4 | from opentelemetry.sdk.resources import SERVICE_NAME, Resource 5 | from opentelemetry.sdk.trace import TracerProvider 6 | from opentelemetry.sdk.trace.export import SimpleSpanProcessor 7 | 8 | 9 | def example_observability_setup_tracing(): 10 | # Create a resource object 11 | resource = Resource(attributes={SERVICE_NAME: "momento_requests_counter"}) 12 | 13 | # Create a tracer provider 14 | tracer_provider = TracerProvider(resource=resource) 15 | 16 | # Create a Zipkin exporter 17 | zipkin_exporter = ZipkinExporter() 18 | 19 | # Create a span processor and add the exporter 20 | span_processor = SimpleSpanProcessor(zipkin_exporter) 21 | tracer_provider.add_span_processor(span_processor) 22 | 23 | # Register the tracer provider 24 | trace.set_tracer_provider(tracer_provider) 25 | 26 | # Register the gRPC instrumentation 27 | GrpcInstrumentorClient().instrument() 28 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/remove_fields.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheDictionaryRemoveFieldsResponse(CacheResponse): 8 | """Parent response type for a cache `dictionary_remove_fields` request. 9 | 10 | Its subtypes are: 11 | - `CacheDictionaryRemoveFields.Success` 12 | - `CacheDictionaryRemoveFields.Error` 13 | 14 | See `CacheClient` for how to work with responses. 15 | """ 16 | 17 | 18 | class CacheDictionaryRemoveFields(ABC): 19 | """Groups all `CacheDictionaryRemoveFieldsResponse` derived types under a common namespace.""" 20 | 21 | class Success(CacheDictionaryRemoveFieldsResponse): 22 | """Indicates the request was successful.""" 23 | 24 | class Error(CacheDictionaryRemoveFieldsResponse, ErrorResponseMixin): 25 | """Contains information about an error returned from a request. 26 | 27 | This includes: 28 | - `error_code`: `MomentoErrorCode` value for the error. 29 | - `messsage`: a detailed error message. 30 | """ 31 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/put_element.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheSortedSetPutElementResponse(CacheResponse): 9 | """Response type for a `sorted_set_put_element` request. 10 | 11 | Its subtypes are: 12 | - `CacheSortedSetPutElement.Success` 13 | - `CacheSortedSetPutElement.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheSortedSetPutElement(ABC): 20 | """Groups all `CacheSortedSetPutElementResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheSortedSetPutElementResponse): 24 | """Indicates the put was successful.""" 25 | 26 | class Error(CacheSortedSetPutElementResponse, ErrorResponseMixin): 27 | """Indicates an error occurred in the request. 28 | 29 | This includes: 30 | - `error_code`: `MomentoErrorCode` value for the error. 31 | - `message`: a detailed error message. 32 | """ 33 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/lib/stack.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import {Construct} from 'constructs'; 4 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 5 | 6 | export class MomentoLambdaStack extends cdk.Stack { 7 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 8 | super(scope, id, props); 9 | 10 | if (!process.env.MOMENTO_API_KEY) { 11 | throw new Error('The environment variable MOMENTO_API_KEY must be set.'); 12 | } 13 | 14 | // Create Lambda function from Docker Image 15 | const dockerLambda = new lambda.DockerImageFunction(this, 'MomentoDockerLambda', { 16 | functionName: 'MomentoDockerLambda', 17 | code: lambda.DockerImageCode.fromImageAsset(path.join(__dirname, '../../docker')), // Point to the root since Dockerfile should be there 18 | environment: { 19 | MOMENTO_API_KEY: process.env.MOMENTO_API_KEY || '' 20 | }, 21 | memorySize: 128, 22 | timeout: cdk.Duration.seconds(30) 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/put_elements.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheSortedSetPutElementsResponse(CacheResponse): 9 | """Response type for a `sorted_set_put_elements` request. 10 | 11 | Its subtypes are: 12 | - `CacheSortedSetPutElements.Success` 13 | - `CacheSortedSetPutElements.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheSortedSetPutElements(ABC): 20 | """Groups all `CacheSortedSetPutElementsResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheSortedSetPutElementsResponse): 24 | """Indicates the put was successful.""" 25 | 26 | class Error(CacheSortedSetPutElementsResponse, ErrorResponseMixin): 27 | """Indicates an error occurred in the request. 28 | 29 | This includes: 30 | - `error_code`: `MomentoErrorCode` value for the error. 31 | - `message`: a detailed error message. 32 | """ 33 | -------------------------------------------------------------------------------- /tests/momento/local/momento_error_code_metadata.py: -------------------------------------------------------------------------------- 1 | from momento.errors import MomentoErrorCode 2 | 3 | MOMENTO_ERROR_CODE_TO_METADATA = { 4 | MomentoErrorCode.INVALID_ARGUMENT_ERROR: "invalid-argument", 5 | MomentoErrorCode.UNKNOWN_SERVICE_ERROR: "unknown", 6 | MomentoErrorCode.ALREADY_EXISTS_ERROR: "already-exists", 7 | MomentoErrorCode.NOT_FOUND_ERROR: "not-found", 8 | MomentoErrorCode.INTERNAL_SERVER_ERROR: "internal", 9 | MomentoErrorCode.PERMISSION_ERROR: "permission-denied", 10 | MomentoErrorCode.AUTHENTICATION_ERROR: "unauthenticated", 11 | MomentoErrorCode.CANCELLED_ERROR: "cancelled", 12 | MomentoErrorCode.LIMIT_EXCEEDED_ERROR: "resource-exhausted", 13 | MomentoErrorCode.BAD_REQUEST_ERROR: "invalid-argument", 14 | MomentoErrorCode.TIMEOUT_ERROR: "deadline-exceeded", 15 | MomentoErrorCode.SERVER_UNAVAILABLE: "unavailable", 16 | MomentoErrorCode.CLIENT_RESOURCE_EXHAUSTED: "resource-exhausted", 17 | MomentoErrorCode.FAILED_PRECONDITION_ERROR: "failed-precondition", 18 | MomentoErrorCode.UNKNOWN_ERROR: "unknown", 19 | MomentoErrorCode.CONNECTION_ERROR: "unavailable", 20 | } 21 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/remove_element.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheSortedSetRemoveElementResponse(CacheResponse): 9 | """Response type for a `sorted_set_remove_element` request. 10 | 11 | Its subtypes are: 12 | - `CacheSortedSetRemoveElement.Success` 13 | - `CacheSortedSetRemoveElement.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheSortedSetRemoveElement(ABC): 20 | """Groups all `CacheSortedSetRemoveElementResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheSortedSetRemoveElementResponse): 24 | """Indicates the remove was successful.""" 25 | 26 | class Error(CacheSortedSetRemoveElementResponse, ErrorResponseMixin): 27 | """Indicates an error occurred in the request. 28 | 29 | This includes: 30 | - `error_code`: `MomentoErrorCode` value for the error. 31 | - `message`: a detailed error message. 32 | """ 33 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/push_back.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheListPushBackResponse(CacheResponse): 9 | """Response type for a `list_push_back` request. 10 | 11 | Its subtypes are: 12 | - `CacheListPushBack.Success` 13 | - `CacheListPushBack.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheListPushBack(ABC): 20 | """Groups all `CacheListPushBackResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheListPushBackResponse): 24 | """Indicates the push was successful.""" 25 | 26 | list_length: int 27 | """The number of values in the list after this push""" 28 | 29 | class Error(CacheListPushBackResponse, ErrorResponseMixin): 30 | """Indicates an error occured in the request. 31 | 32 | This includes: 33 | - `error_code`: `MomentoErrorCode` value for the error. 34 | - `messsage`: a detailed error message. 35 | """ 36 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/remove_elements.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheSortedSetRemoveElementsResponse(CacheResponse): 9 | """Response type for a `sorted_set_remove_elements` request. 10 | 11 | Its subtypes are: 12 | - `CacheSortedSetRemoveElements.Success` 13 | - `CacheSortedSetRemoveElements.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheSortedSetRemoveElements(ABC): 20 | """Groups all `CacheSortedSetRemoveElementsResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheSortedSetRemoveElementsResponse): 24 | """Indicates the remove was successful.""" 25 | 26 | class Error(CacheSortedSetRemoveElementsResponse, ErrorResponseMixin): 27 | """Indicates an error occurred in the request. 28 | 29 | This includes: 30 | - `error_code`: `MomentoErrorCode` value for the error. 31 | - `message`: a detailed error message. 32 | """ 33 | -------------------------------------------------------------------------------- /src/momento/responses/control/signing_key/revoke.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | 6 | from momento.responses.response import ControlResponse 7 | 8 | from ...mixins import ErrorResponseMixin 9 | 10 | 11 | class RevokeSigningKeyResponse(ControlResponse): 12 | """Parent response type for a cache `revoke_signing_key` request. 13 | 14 | Its subtypes are: 15 | - `RevokeSigningKey.Success` 16 | - `RevokeSigningKey.Error` 17 | 18 | See `CacheClient` for how to work with responses. 19 | """ 20 | 21 | 22 | class RevokeSigningKey(ABC): 23 | """Groups all `RevokeSigningKeyResponse` derived types under a common namespace.""" 24 | 25 | @dataclass 26 | class Success(RevokeSigningKeyResponse): 27 | """The response from revoking a signing key.""" 28 | 29 | class Error(RevokeSigningKeyResponse, ErrorResponseMixin): 30 | """Contains information about an error returned from a request. 31 | 32 | This includes: 33 | - `error_code`: `MomentoErrorCode` value for the error. 34 | - `messsage`: a detailed error message. 35 | """ 36 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/push_front.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheListPushFrontResponse(CacheResponse): 9 | """Response type for a `list_push_front` request. 10 | 11 | Its subtypes are: 12 | - `CacheListPushFront.Success` 13 | - `CacheListPushFront.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheListPushFront(ABC): 20 | """Groups all `CacheListPushFrontResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheListPushFrontResponse): 24 | """Indicates the push was successful.""" 25 | 26 | list_length: int 27 | """The number of values in the list after this push""" 28 | 29 | class Error(CacheListPushFrontResponse, ErrorResponseMixin): 30 | """Indicates an error occured in the request. 31 | 32 | This includes: 33 | - `error_code`: `MomentoErrorCode` value for the error. 34 | - `messsage`: a detailed error message. 35 | """ 36 | -------------------------------------------------------------------------------- /src/momento/__init__.py: -------------------------------------------------------------------------------- 1 | """Momento client library. 2 | 3 | Instantiate a client with `CacheClient` or `CacheClientAsync` (for asyncio). 4 | Use `CredentialProvider` to read credentials for the client. 5 | Use `Configurations` for pre-built network configurations. 6 | """ 7 | 8 | import logging 9 | 10 | from momento import logs 11 | 12 | from .auth import CredentialProvider 13 | from .auth_client import AuthClient 14 | from .auth_client_async import AuthClientAsync 15 | from .cache_client import CacheClient 16 | from .cache_client_async import CacheClientAsync 17 | from .config import Configurations, TopicConfigurations 18 | from .topic_client import TopicClient 19 | from .topic_client_async import TopicClientAsync 20 | 21 | __version__ = "1.28.0" # x-release-please-version 22 | 23 | logging.getLogger("momentosdk").addHandler(logging.NullHandler()) 24 | logs.initialize_momento_logging() 25 | 26 | __all__ = [ 27 | "CredentialProvider", 28 | "Configurations", 29 | "TopicConfigurations", 30 | "CacheClient", 31 | "CacheClientAsync", 32 | "TopicClient", 33 | "TopicClientAsync", 34 | "AuthClient", 35 | "AuthClientAsync", 36 | "__version__", 37 | ] 38 | -------------------------------------------------------------------------------- /examples/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | prometheus_data: {} 5 | grafana_data: {} 6 | 7 | services: 8 | prometheus: 9 | image: prom/prometheus 10 | container_name: prometheus 11 | restart: always 12 | volumes: 13 | - ./config/prometheus.yml:/etc/prometheus/prometheus.yml 14 | - prometheus_data:/prometheus 15 | command: 16 | - '--config.file=/etc/prometheus/prometheus.yml' 17 | - '--storage.tsdb.path=/prometheus' 18 | - '--web.console.libraries=/usr/share/prometheus/console_libraries' 19 | - '--web.console.templates=/usr/share/prometheus/consoles' 20 | ports: 21 | - 9090:9090 22 | 23 | zipkin: 24 | image: openzipkin/zipkin 25 | container_name: zipkin 26 | ports: 27 | - 9411:9411 28 | 29 | grafana: 30 | image: grafana/grafana:latest 31 | container_name: grafana 32 | environment: 33 | - GF_SECURITY_ADMIN_USER=admin 34 | - GF_SECURITY_ADMIN_PASSWORD=grafana 35 | volumes: 36 | - ./grafana/provisioning:/etc/grafana/provisioning 37 | - ./grafana/dashboards:/var/lib/grafana/dashboards 38 | ports: 39 | - "3000:3000" 40 | depends_on: 41 | - prometheus 42 | -------------------------------------------------------------------------------- /tests/momento/responses/test_cache_dictionary_fetch_response.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from momento.responses import CacheDictionaryFetch 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "dictionary, expected_str", 9 | [ 10 | ({b"hello": b"world"}, "CacheDictionaryFetch.Hit(value_dictionary_bytes_bytes={b'hello': b'world'})"), 11 | ( 12 | {("i" * 100).encode(): ("i" * 100).encode()}, 13 | "CacheDictionaryFetch.Hit(value_dictionary_bytes_bytes={b'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii...': b'iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii...'})", # noqa: E501 14 | ), 15 | ( 16 | {f"{i}".encode(): f"{i}".encode() for i in range(10)}, 17 | "CacheDictionaryFetch.Hit(value_dictionary_bytes_bytes={b'7': b'7', b'2': b'2', b'6': b'6', b'9': b'9', b'3': b'3', ...})", # noqa: E501 18 | ), 19 | ], 20 | ) 21 | def test_dictionary_fetch_hit_str_and_repr(dictionary: dict[bytes, bytes], expected_str: str) -> None: 22 | hit = CacheDictionaryFetch.Hit(dictionary) 23 | if "..." in expected_str: 24 | assert "..." in str(hit) 25 | else: 26 | assert str(hit) == expected_str 27 | assert eval(repr(hit)) == hit 28 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/increment.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheDictionaryIncrementResponse(CacheResponse): 9 | """Parent response type for a cache `dictionary_increment` request. 10 | 11 | Its subtypes are: 12 | - `CacheDictionaryIncrement.Success` 13 | - `CacheDictionaryIncrement.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheDictionaryIncrement(ABC): 20 | """Groups all `CacheDictionaryIncrementResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheDictionaryIncrementResponse): 24 | """Indicates the request was successful.""" 25 | 26 | value: int 27 | """The value of the field post-increment.""" 28 | 29 | class Error(CacheDictionaryIncrementResponse, ErrorResponseMixin): 30 | """Contains information about an error returned from a request. 31 | 32 | This includes: 33 | - `error_code`: `MomentoErrorCode` value for the error. 34 | - `messsage`: a detailed error message. 35 | """ 36 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/concatenate_back.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheListConcatenateBackResponse(CacheResponse): 9 | """Response type for a `list_concatenate_back` request. 10 | 11 | Its subtypes are: 12 | - `CacheListConcatenateBack.Success` 13 | - `CacheListConcatenateBack.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheListConcatenateBack(ABC): 20 | """Groups all `CacheListConcatenateBackResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheListConcatenateBackResponse): 24 | """Indicates the concatenation was successful.""" 25 | 26 | list_length: int 27 | """The number of values in the list after this concatenation""" 28 | 29 | class Error(CacheListConcatenateBackResponse, ErrorResponseMixin): 30 | """Indicates an error occurred in the request. 31 | 32 | This includes: 33 | - `error_code`: `MomentoErrorCode` value for the error. 34 | - `messsage`: a detailed error message. 35 | """ 36 | -------------------------------------------------------------------------------- /src/momento/responses/data/scalar/set_if_not_exists.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from ...mixins import ErrorResponseMixin 4 | from ...response import CacheResponse 5 | 6 | 7 | class CacheSetIfNotExistsResponse(CacheResponse): 8 | """Parent response type for a cache `set_if_not_exists` request. 9 | 10 | Its subtypes are: 11 | - `CacheSetIfNotExists.Stored` 12 | - `CacheSetIfNotExists.NotStored` 13 | - `CacheSetIfNotExists.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheSetIfNotExists(ABC): 20 | """Groups all `CacheSetIfNotExistsResponse` derived types under a common namespace.""" 21 | 22 | class Stored(CacheSetIfNotExistsResponse): 23 | """Indicates the key did not exist and the value was set.""" 24 | 25 | class NotStored(CacheSetIfNotExistsResponse): 26 | """Indicates the key existed and no value was set.""" 27 | 28 | class Error(CacheSetIfNotExistsResponse, ErrorResponseMixin): 29 | """Contains information about an error returned from a request. 30 | 31 | This includes: 32 | - `error_code`: `MomentoErrorCode` value for the error. 33 | - `messsage`: a detailed error message. 34 | """ 35 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/concatenate_front.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheListConcatenateFrontResponse(CacheResponse): 9 | """Response type for a `list_concatenate_front` request. 10 | 11 | Its subtypes are: 12 | - `CacheListConcatenateFront.Success` 13 | - `CacheListConcatenateFront.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheListConcatenateFront(ABC): 20 | """Groups all `CacheListConcatenateFrontResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheListConcatenateFrontResponse): 24 | """Indicates the concatenation was successful.""" 25 | 26 | list_length: int 27 | """The number of values in the list after this concatenation""" 28 | 29 | class Error(CacheListConcatenateFrontResponse, ErrorResponseMixin): 30 | """Indicates an error occurred in the request. 31 | 32 | This includes: 33 | - `error_code`: `MomentoErrorCode` value for the error. 34 | - `messsage`: a detailed error message. 35 | """ 36 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/length.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheListLengthResponse(CacheResponse): 9 | """Response type for a `list_length` request. 10 | 11 | Its subtypes are: 12 | - `CacheListLength.Hit` 13 | - `CacheListLength.Miss` 14 | - `CacheListLength.Error` 15 | 16 | See `CacheClient` for how to work with responses. 17 | """ 18 | 19 | 20 | class CacheListLength(ABC): 21 | """Groups all `CacheListLengthResponse` derived types under a common namespace.""" 22 | 23 | @dataclass 24 | class Hit(CacheListLengthResponse): 25 | """Indicates the list exists and its length was fetched.""" 26 | 27 | length: int 28 | """The number of values in the list.""" 29 | 30 | class Miss(CacheListLengthResponse): 31 | """Indicates the list does not exist.""" 32 | 33 | class Error(CacheListLengthResponse, ErrorResponseMixin): 34 | """Indicates an error occured in the request. 35 | 36 | This includes 37 | - `error_code`: `MomentoErrorCode` value for the error. 38 | - `messsage`: a detailed error message. 39 | """ 40 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/increment_score.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheSortedSetIncrementScoreResponse(CacheResponse): 9 | """Parent response type for a cache `sorted_set_increment_score` request. 10 | 11 | Its subtypes are: 12 | - `CacheSortedSetIncrementScore.Success` 13 | - `CacheSortedSetIncrementScore.Error` 14 | 15 | See `CacheClient` for how to work with responses. 16 | """ 17 | 18 | 19 | class CacheSortedSetIncrementScore(ABC): 20 | """Groups all `CacheSortedSetIncrementScoreResponse` derived types under a common namespace.""" 21 | 22 | @dataclass 23 | class Success(CacheSortedSetIncrementScoreResponse): 24 | """Indicates the request was successful.""" 25 | 26 | score: float 27 | """The score of the element post-increment.""" 28 | 29 | class Error(CacheSortedSetIncrementScoreResponse, ErrorResponseMixin): 30 | """Contains information about an error returned from a request. 31 | 32 | This includes: 33 | - `error_code`: `MomentoErrorCode` value for the error. 34 | - `message`: a detailed error message. 35 | """ 36 | -------------------------------------------------------------------------------- /.github/workflows/momento-local-tests.yml: -------------------------------------------------------------------------------- 1 | name: Momento Local tests 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | local-tests: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-24.04] 12 | python-version: ["3.13"] 13 | runs-on: ${{ matrix.os }} 14 | 15 | env: 16 | TEST_API_KEY: ${{ secrets.ALPHA_TEST_AUTH_TOKEN }} 17 | TEST_CACHE_NAME: python-integration-test-${{ matrix.python-version }}-${{ matrix.new-python-protobuf }}-${{ github.sha }} 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Setup Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install and configure Poetry 28 | uses: snok/install-poetry@v1 29 | with: 30 | version: 1.3.1 31 | virtualenvs-in-project: true 32 | 33 | - name: Install dependencies 34 | run: poetry install 35 | 36 | - name: Start Momento Local 37 | run: | 38 | docker run --cap-add=NET_ADMIN --rm -d -p 8080:8080 -p 9090:9090 gomomento/momento-local --enable-test-admin 39 | 40 | - name: Run tests 41 | run: poetry run pytest -p no:sugar -q -m local 42 | -------------------------------------------------------------------------------- /examples/prepy310/topic_publish.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | 4 | from momento import ( 5 | CacheClient, 6 | Configurations, 7 | CredentialProvider, 8 | TopicClient, 9 | TopicConfigurations, 10 | ) 11 | from momento.responses import CreateCache, TopicPublish 12 | 13 | from example_utils.example_logging import initialize_logging 14 | 15 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 16 | _CACHE_NAME = "cache" 17 | _logger = logging.getLogger("topic-publish-example") 18 | 19 | 20 | def setup_cache() -> None: 21 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 22 | response = client.create_cache(_CACHE_NAME) 23 | if isinstance(response, CreateCache.Error): 24 | raise response.inner_exception 25 | 26 | 27 | def main() -> None: 28 | initialize_logging() 29 | setup_cache() 30 | _logger.info("hello") 31 | with TopicClient(TopicConfigurations.Default.v1(), _AUTH_PROVIDER) as client: 32 | response = client.publish("cache", "my_topic", "my_value") 33 | if isinstance(response, TopicPublish.Error): 34 | print("error: ", response.message) 35 | 36 | 37 | if __name__ == "__main__": 38 | main() 39 | -------------------------------------------------------------------------------- /src/momento/config/transport/grpc_configuration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from datetime import timedelta 5 | from pathlib import Path 6 | from typing import Optional 7 | 8 | 9 | class GrpcConfiguration(ABC): 10 | @abstractmethod 11 | def get_deadline(self) -> timedelta: 12 | pass 13 | 14 | @abstractmethod 15 | def with_deadline(self, deadline: timedelta) -> GrpcConfiguration: 16 | pass 17 | 18 | @abstractmethod 19 | def with_root_certificates_pem(self, root_certificates_pem_path: Path) -> GrpcConfiguration: 20 | pass 21 | 22 | @abstractmethod 23 | def get_root_certificates_pem(self) -> Optional[bytes]: 24 | pass 25 | 26 | @abstractmethod 27 | def get_max_send_message_length(self) -> Optional[int]: 28 | pass 29 | 30 | @abstractmethod 31 | def get_max_receive_message_length(self) -> Optional[int]: 32 | pass 33 | 34 | @abstractmethod 35 | def get_keepalive_permit_without_calls(self) -> Optional[int]: 36 | pass 37 | 38 | @abstractmethod 39 | def get_keepalive_time(self) -> Optional[timedelta]: 40 | pass 41 | 42 | @abstractmethod 43 | def get_keepalive_timeout(self) -> Optional[timedelta]: 44 | pass 45 | -------------------------------------------------------------------------------- /src/momento/config/middleware/synchronous/middleware.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from momento.config.middleware.models import MiddlewareMessage, MiddlewareRequestHandlerContext, MiddlewareStatus 4 | from momento.config.middleware.synchronous.middleware_metadata import MiddlewareMetadata 5 | 6 | 7 | class MiddlewareRequestHandler(abc.ABC): 8 | @abc.abstractmethod 9 | def on_request_metadata(self, metadata: MiddlewareMetadata) -> MiddlewareMetadata: 10 | pass 11 | 12 | @abc.abstractmethod 13 | def on_request_body(self, request: MiddlewareMessage) -> MiddlewareMessage: 14 | pass 15 | 16 | @abc.abstractmethod 17 | def on_response_metadata(self, metadata: MiddlewareMetadata) -> MiddlewareMetadata: 18 | pass 19 | 20 | @abc.abstractmethod 21 | def on_response_body(self, response: MiddlewareMessage) -> MiddlewareMessage: 22 | pass 23 | 24 | @abc.abstractmethod 25 | def on_response_status(self, status: MiddlewareStatus) -> MiddlewareStatus: 26 | pass 27 | 28 | 29 | class Middleware(abc.ABC): 30 | @abc.abstractmethod 31 | def on_new_request(self, context: MiddlewareRequestHandlerContext) -> MiddlewareRequestHandler: 32 | pass 33 | 34 | # noinspection PyMethodMayBeStatic 35 | def close(self) -> None: 36 | return None 37 | -------------------------------------------------------------------------------- /examples/py310/topic_publish.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | 4 | from momento import ( 5 | CacheClient, 6 | Configurations, 7 | CredentialProvider, 8 | TopicClient, 9 | TopicConfigurations, 10 | ) 11 | from momento.responses import CreateCache, TopicPublish 12 | 13 | from example_utils.example_logging import initialize_logging 14 | 15 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 16 | _CACHE_NAME = "cache" 17 | _logger = logging.getLogger("topic-publish-example") 18 | 19 | 20 | def setup_cache() -> None: 21 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 22 | response = client.create_cache(_CACHE_NAME) 23 | match response: 24 | case CreateCache.Error(): 25 | raise response.inner_exception 26 | 27 | 28 | def main() -> None: 29 | initialize_logging() 30 | setup_cache() 31 | _logger.info("hello") 32 | with TopicClient(TopicConfigurations.Default.v1(), _AUTH_PROVIDER) as client: 33 | response = client.publish("cache", "my_topic", "my_value") 34 | match response: 35 | case TopicPublish.Error(): 36 | print("error: ", response.message) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /src/momento/config/middleware/aio/middleware.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from momento.config.middleware.aio.middleware_metadata import MiddlewareMetadata 4 | from momento.config.middleware.models import MiddlewareMessage, MiddlewareRequestHandlerContext, MiddlewareStatus 5 | 6 | 7 | class MiddlewareRequestHandler(abc.ABC): 8 | @abc.abstractmethod 9 | async def on_request_metadata(self, metadata: MiddlewareMetadata) -> MiddlewareMetadata: 10 | pass 11 | 12 | @abc.abstractmethod 13 | async def on_request_body(self, request: MiddlewareMessage) -> MiddlewareMessage: 14 | pass 15 | 16 | @abc.abstractmethod 17 | async def on_response_metadata(self, metadata: MiddlewareMetadata) -> MiddlewareMetadata: 18 | pass 19 | 20 | @abc.abstractmethod 21 | async def on_response_body(self, response: MiddlewareMessage) -> MiddlewareMessage: 22 | pass 23 | 24 | @abc.abstractmethod 25 | async def on_response_status(self, status: MiddlewareStatus) -> MiddlewareStatus: 26 | pass 27 | 28 | 29 | class Middleware(abc.ABC): 30 | @abc.abstractmethod 31 | async def on_new_request(self, context: MiddlewareRequestHandlerContext) -> MiddlewareRequestHandler: 32 | pass 33 | 34 | # noinspection PyMethodMayBeStatic 35 | def close(self) -> None: 36 | return None 37 | -------------------------------------------------------------------------------- /src/momento/config/topic_configurations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import timedelta 4 | 5 | from momento.config.transport.topic_transport_strategy import StaticTopicGrpcConfiguration, StaticTopicTransportStrategy 6 | 7 | from .topic_configuration import TopicConfiguration 8 | 9 | 10 | class TopicConfigurations: 11 | class Default(TopicConfiguration): 12 | """Provides the recommended default configuration for topic clients.""" 13 | 14 | @staticmethod 15 | def latest() -> TopicConfigurations.Default: 16 | """Provides the latest recommended configuration for topics. 17 | 18 | This configuration will be updated every time there is a new version of the laptop configuration. 19 | """ 20 | return TopicConfigurations.Default.v1() 21 | 22 | @staticmethod 23 | def v1() -> TopicConfigurations.Default: 24 | """Provides the v1 recommended configuration for topics. 25 | 26 | This configuration is guaranteed not to change in future releases of the Momento Python SDK. 27 | """ 28 | return TopicConfigurations.Default( 29 | StaticTopicTransportStrategy(StaticTopicGrpcConfiguration(deadline=timedelta(seconds=5))), 30 | max_subscriptions=0, 31 | ) 32 | -------------------------------------------------------------------------------- /src/momento/responses/data/scalar/get.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin, ValueStringMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheGetResponse(CacheResponse): 9 | """Parent response type for a cache `get` request. 10 | 11 | Its subtypes are: 12 | - `CacheGet.Hit` 13 | - `CacheGet.Miss` 14 | - `CacheGet.Error` 15 | 16 | See `CacheClient` for how to work with responses. 17 | """ 18 | 19 | 20 | class CacheGet(ABC): 21 | """Groups all `CacheGetResponse` derived types under a common namespace.""" 22 | 23 | @dataclass 24 | class Hit(CacheGetResponse, ValueStringMixin): 25 | """Contains the result of a cache hit.""" 26 | 27 | value_bytes: bytes 28 | """The value returned from the cache for the specified key. Use the 29 | `value_string` property to access the value as a string.""" 30 | 31 | class Miss(CacheGetResponse): 32 | """Contains the results of a cache miss.""" 33 | 34 | class Error(CacheGetResponse, ErrorResponseMixin): 35 | """Contains information about an error returned from a request. 36 | 37 | This includes: 38 | - `error_code`: `MomentoErrorCode` value for the error. 39 | - `messsage`: a detailed error message. 40 | """ 41 | -------------------------------------------------------------------------------- /src/momento/responses/data/scalar/increment.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from momento.errors import SdkException 5 | 6 | from ...mixins import ErrorResponseMixin 7 | from ...response import CacheResponse 8 | 9 | 10 | class CacheIncrementResponse(CacheResponse): 11 | """Parent response type for a cache `increment` request. 12 | 13 | Its subtypes are: 14 | - `CacheIncrement.Success` 15 | - `CacheIncrement.Error` 16 | 17 | See `CacheClient` for how to work with responses. 18 | """ 19 | 20 | 21 | class CacheIncrement(ABC): 22 | """Groups all `CacheIncrementResponse` derived types under a common namespace.""" 23 | 24 | @dataclass 25 | class Success(CacheIncrementResponse): 26 | """Indicates the increment was successful.""" 27 | 28 | value: int 29 | """The value of the field post-increment.""" 30 | 31 | @dataclass 32 | class Error(CacheIncrementResponse, ErrorResponseMixin): 33 | """Contains information about an error returned from a request. 34 | 35 | This includes: 36 | - `error_code`: `MomentoErrorCode` value for the error. 37 | - `messsage`: a detailed error message. 38 | """ 39 | 40 | _error: SdkException 41 | 42 | def __init__(self, _error: SdkException): 43 | self._error = _error 44 | -------------------------------------------------------------------------------- /examples/prepy310/topic_publish_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from datetime import timedelta 4 | 5 | from momento import ( 6 | CacheClient, 7 | Configurations, 8 | CredentialProvider, 9 | TopicClientAsync, 10 | TopicConfigurations, 11 | ) 12 | from momento.responses import CreateCache, TopicPublish 13 | 14 | from example_utils.example_logging import initialize_logging 15 | 16 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 17 | _CACHE_NAME = "cache" 18 | _logger = logging.getLogger("topic-publish-example") 19 | 20 | 21 | def setup_cache() -> None: 22 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 23 | response = client.create_cache(_CACHE_NAME) 24 | if isinstance(response, CreateCache.Error): 25 | raise response.inner_exception 26 | 27 | 28 | async def main() -> None: 29 | initialize_logging() 30 | setup_cache() 31 | _logger.info("hello") 32 | async with TopicClientAsync(TopicConfigurations.Default.v1(), _AUTH_PROVIDER) as client: 33 | response = await client.publish("cache", "my_topic", "my_value") 34 | if isinstance(response, TopicPublish.Error): 35 | print("error: ", response.message) 36 | 37 | 38 | if __name__ == "__main__": 39 | asyncio.run(main()) 40 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/pop_back.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin, ValueStringMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheListPopBackResponse(CacheResponse): 9 | """Parent response type for a `list_pop_back` request. 10 | 11 | Its subtypes are: 12 | - `CacheListPopBack.Hit` 13 | - `CacheListPopBack.Miss` 14 | - `CacheListPopBack.Error` 15 | 16 | See `CacheClient` for how to work with responses. 17 | """ 18 | 19 | 20 | class CacheListPopBack(ABC): 21 | """Groups all `CacheListPopBack` derived types under a common namespace.""" 22 | 23 | @dataclass 24 | class Hit(CacheListPopBackResponse, ValueStringMixin): 25 | """Indicates the request was successful.""" 26 | 27 | value_bytes: bytes 28 | """The value popped from the list. Use the `value_string` property to access the value as a string.""" 29 | 30 | class Miss(CacheListPopBackResponse): 31 | """Indicates the list does not exist.""" 32 | 33 | class Error(CacheListPopBackResponse, ErrorResponseMixin): 34 | """Contains information about an error returned from a request. 35 | 36 | This includes: 37 | - `error_code`: `MomentoErrorCode` value for the error. 38 | - `messsage`: a detailed error message. 39 | """ 40 | -------------------------------------------------------------------------------- /examples/py310/topic_publish_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from datetime import timedelta 4 | 5 | from momento import ( 6 | CacheClient, 7 | Configurations, 8 | CredentialProvider, 9 | TopicClientAsync, 10 | TopicConfigurations, 11 | ) 12 | from momento.responses import CreateCache, TopicPublish 13 | 14 | from example_utils.example_logging import initialize_logging 15 | 16 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 17 | _CACHE_NAME = "cache" 18 | _logger = logging.getLogger("topic-publish-example") 19 | 20 | 21 | def setup_cache() -> None: 22 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 23 | response = client.create_cache(_CACHE_NAME) 24 | match response: 25 | case CreateCache.Error(): 26 | raise response.inner_exception 27 | 28 | 29 | async def main() -> None: 30 | initialize_logging() 31 | setup_cache() 32 | _logger.info("hello") 33 | async with TopicClientAsync(TopicConfigurations.Default.v1(), _AUTH_PROVIDER) as client: 34 | response = await client.publish("cache", "my_topic", "my_value") 35 | match response: 36 | case TopicPublish.Error(): 37 | print("error: ", response.message) 38 | 39 | 40 | if __name__ == "__main__": 41 | asyncio.run(main()) 42 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/pop_front.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin, ValueStringMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheListPopFrontResponse(CacheResponse): 9 | """Parent response type for a `list_pop_front` request. 10 | 11 | Its subtypes are: 12 | - `CacheListPopFront.Hit` 13 | - `CacheListPopFront.Miss` 14 | - `CacheListPopFront.Error` 15 | 16 | See `CacheClient` for how to work with responses. 17 | """ 18 | 19 | 20 | class CacheListPopFront(ABC): 21 | """Groups all `CacheListPopFront` derived types under a common namespace.""" 22 | 23 | @dataclass 24 | class Hit(CacheListPopFrontResponse, ValueStringMixin): 25 | """Indicates the request was successful.""" 26 | 27 | value_bytes: bytes 28 | """The value popped from the list. Use the `value_string` property to access the value as a string.""" 29 | 30 | class Miss(CacheListPopFrontResponse): 31 | """Indicates the list does not exist.""" 32 | 33 | class Error(CacheListPopFrontResponse, ErrorResponseMixin): 34 | """Contains information about an error returned from a request. 35 | 36 | This includes: 37 | - `error_code`: `MomentoErrorCode` value for the error. 38 | - `messsage`: a detailed error message. 39 | """ 40 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/get_rank.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheSortedSetGetRankResponse(CacheResponse): 9 | """Parent response type for a cache `sorted_set_get_rank` request. 10 | 11 | Its subtypes are: 12 | - `CacheSortedSetGetRank.Hit` 13 | - `CacheSortedSetGetRank.Miss` 14 | - `CacheSortedSetGetRank.Error` 15 | 16 | See `CacheClient` for how to work with responses. 17 | """ 18 | 19 | 20 | class CacheSortedSetGetRank(ABC): 21 | """Groups all `CacheSortedSetGetRankResponse` derived types under a common namespace.""" 22 | 23 | @dataclass 24 | class Hit(CacheSortedSetGetRankResponse): 25 | """Contains the result of a cache hit.""" 26 | 27 | rank: float 28 | """The rank of the sorted set value that was queried.""" 29 | 30 | @dataclass 31 | class Miss(CacheSortedSetGetRankResponse): 32 | """Indicates the sorted set or sorted set element does not exist.""" 33 | 34 | class Error(CacheSortedSetGetRankResponse, ErrorResponseMixin): 35 | """Contains information about an error returned from a request. 36 | 37 | This includes: 38 | - `error_code`: `MomentoErrorCode` value for the error. 39 | - `message`: a detailed error message. 40 | """ 41 | -------------------------------------------------------------------------------- /examples/lambda/zip/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: setup 2 | setup: 3 | python -m venv .venv 4 | .venv/bin/pip install --upgrade pip 5 | .venv/bin/pip install -r requirements.txt 6 | 7 | .PHONY: dist 8 | dist: 9 | rm -rf dist dist.zip 10 | # Some important notes: 11 | # 1. The `--platform` option is used to specify the target platform. In this case, we are targeting the manylinux2014_x86_64 platform for AWS Lambda. 12 | # 2. The `--target` option is used to specify the target directory where the dependencies will be installed. 13 | # 3. The `--implementation` option is used to specify the Python implementation. In this case, we are using the CPython implementation. 14 | # 4. The `--python-version` option is used to specify the Python version. In this case, we are using Python 3.8. You can change this to match the Python version used by AWS Lambda. 15 | # 5. The `--only-binary` option is used to specify that only binary distributions should be used. This is important because AWS Lambda does not support building from source. 16 | # 6. The `--upgrade` option is used to ensure that the dependencies are up-to-date. 17 | # 7. The `-r requirements.txt` option is used to specify the requirements file that contains the dependencies to install. 18 | .venv/bin/pip install \ 19 | --platform manylinux2014_x86_64 \ 20 | --target=dist \ 21 | --implementation cp \ 22 | --python-version 3.8 \ 23 | --only-binary=:all: \ 24 | --upgrade \ 25 | -r requirements.txt 26 | cp index.py dist 27 | cd dist && zip -9r ../dist.zip . 28 | -------------------------------------------------------------------------------- /src/momento/responses/data/list/fetch.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | 6 | from ...mixins import ErrorResponseMixin 7 | from ...response import CacheResponse 8 | 9 | 10 | class CacheListFetchResponse(CacheResponse): 11 | """Response type for a `list_fetch` request. 12 | 13 | Its subtypes are: 14 | - `CacheListFetch.Hit` 15 | - `CacheListFetch.Miss` 16 | - `CacheListFetch.Error` 17 | 18 | See `CacheClient` for how to work with responses. 19 | """ 20 | 21 | 22 | class CacheListFetch(ABC): 23 | """Groups all `CacheListFetchResponse` derived types under a common namespace.""" 24 | 25 | @dataclass 26 | class Hit(CacheListFetchResponse): 27 | """Indicates the list exists and its values were fetched.""" 28 | 29 | value_list_bytes: list[bytes] 30 | """The values for the fetched list, as bytes.""" 31 | 32 | @property 33 | def value_list_string(self) -> list[str]: 34 | """The values for the fetched list, as utf-8 encoded strings. 35 | 36 | Returns: 37 | list[str] 38 | """ 39 | return [v.decode("utf-8") for v in self.value_list_bytes] 40 | 41 | class Miss(CacheListFetchResponse): 42 | """Indicates the list does not exist.""" 43 | 44 | class Error(CacheListFetchResponse, ErrorResponseMixin): 45 | """Indicates an error occured in the request. 46 | 47 | This includes: 48 | - `error_code`: `MomentoErrorCode` value for the error. 49 | - `messsage`: a detailed error message. 50 | """ 51 | -------------------------------------------------------------------------------- /src/momento/responses/control/cache/delete.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from momento.responses.response import ControlResponse 4 | 5 | from ...mixins import ErrorResponseMixin 6 | 7 | 8 | class DeleteCacheResponse(ControlResponse): 9 | """Parent response type for a delete cache request. 10 | 11 | The response object is resolved to a type-safe object of one of 12 | the following subtypes: 13 | - `DeleteCache.Success` 14 | - `DeleteCache.Error` 15 | 16 | Pattern matching can be used to operate on the appropriate subtype. 17 | For example, in python 3.10+:: 18 | 19 | match response: 20 | case DeleteCache.Success(): 21 | ... 22 | case DeleteCache.Error(): 23 | ... 24 | case _: 25 | # Shouldn't happen 26 | 27 | or equivalently in earlier versions of python:: 28 | 29 | if isinstance(response, DeleteCache.Success): 30 | ... 31 | elif isinstance(response, DeleteCache.Error): 32 | ... 33 | else: 34 | # Shouldn't happen 35 | """ 36 | 37 | 38 | class DeleteCache(ABC): 39 | """Groups all `DeleteCacheResponse` derived types under a common namespace.""" 40 | 41 | class Success(DeleteCacheResponse): 42 | """Indicates the request was successful.""" 43 | 44 | class Error(DeleteCacheResponse, ErrorResponseMixin): 45 | """Contains information about an error returned from a request. 46 | 47 | This includes: 48 | - `error_code`: `MomentoErrorCode` value for the error. 49 | - `messsage`: a detailed error message. 50 | """ 51 | -------------------------------------------------------------------------------- /README.template.md: -------------------------------------------------------------------------------- 1 | {{ ossHeader }} 2 | 3 | ## Packages 4 | 5 | The Momento Python SDK package is available on pypi: [momento](https://pypi.org/project/momento/). 6 | 7 | ## Usage 8 | 9 | The examples below require an environment variable named `MOMENTO_API_KEY` which must 10 | be set to a valid Momento API key. You can get one from the [Momento Console](https://console.gomomento.com). 11 | 12 | Python 3.10 introduced the `match` statement, which allows for [structural pattern matching on objects](https://peps.python.org/pep-0636/#adding-a-ui-matching-objects). 13 | If you are running python 3.10 or greater, here is a quickstart you can use in your own project: 14 | 15 | ```python 16 | {% include "./examples/py310/readme.py" %} 17 | ``` 18 | 19 | The above code uses [structural pattern matching](https://peps.python.org/pep-0636/), a feature introduced in Python 3.10. 20 | Using a Python version less than 3.10? No problem. Here is the same example compatible across all versions of Python: 21 | 22 | ```python 23 | {% include "./examples/prepy310/readme.py" %} 24 | ``` 25 | 26 | ## Getting Started and Documentation 27 | 28 | Documentation is available on the [Momento Docs website](https://docs.momentohq.com). 29 | 30 | ## Examples 31 | 32 | Working example projects, with all required build configuration files, are available for both Python 3.10 and up 33 | and Python versions before 3.10: 34 | 35 | * [Python 3.10+ examples](./examples/py310) 36 | * [Pre-3.10 Python examples](./examples/prepy310) 37 | 38 | ## Developing 39 | 40 | If you are interested in contributing to the SDK, please see the [CONTRIBUTING](./CONTRIBUTING.md) docs. 41 | 42 | {{ ossFooter }} 43 | -------------------------------------------------------------------------------- /examples/prepy310/quickstart.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from momento import CacheClient, Configurations, CredentialProvider 4 | from momento.responses import CacheGet, CacheSet, CreateCache 5 | 6 | if __name__ == "__main__": 7 | cache_name = "default-cache" 8 | with CacheClient.create( 9 | configuration=Configurations.Laptop.v1(), 10 | credential_provider=CredentialProvider.from_environment_variable("MOMENTO_API_KEY"), 11 | default_ttl=timedelta(seconds=60), 12 | ) as cache_client: 13 | create_cache_response = cache_client.create_cache(cache_name) 14 | if isinstance(create_cache_response, CreateCache.CacheAlreadyExists): 15 | print(f"Cache with name: {cache_name} already exists.") 16 | elif isinstance(create_cache_response, CreateCache.Error): 17 | raise create_cache_response.inner_exception 18 | 19 | print("Setting Key: foo to Value: FOO") 20 | set_response = cache_client.set(cache_name, "foo", "FOO") 21 | if isinstance(set_response, CacheSet.Error): 22 | raise set_response.inner_exception 23 | 24 | print("Getting Key: foo") 25 | get_response = cache_client.get(cache_name, "foo") 26 | if isinstance(get_response, CacheGet.Hit): 27 | print(f"Look up resulted in a hit: {get_response.value_string}") 28 | print(f"Looked up Value: {get_response.value_string}") 29 | elif isinstance(get_response, CacheGet.Miss): 30 | print("Look up resulted in a: miss. This is unexpected.") 31 | elif isinstance(get_response, CacheGet.Error): 32 | raise get_response.inner_exception 33 | -------------------------------------------------------------------------------- /examples/lambda/zip/index.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from momento import CacheClient, Configurations, CredentialProvider 4 | from momento.responses import CacheGet, CacheSet, CreateCache 5 | 6 | 7 | def handler(event, lambda_context): 8 | cache_name = "default-cache" 9 | with CacheClient.create( 10 | configuration=Configurations.Lambda.latest(), 11 | credential_provider=CredentialProvider.from_environment_variable("MOMENTO_API_KEY"), 12 | default_ttl=timedelta(seconds=60), 13 | ) as cache_client: 14 | create_cache_response = cache_client.create_cache(cache_name) 15 | 16 | if isinstance(create_cache_response, CreateCache.CacheAlreadyExists): 17 | print(f"Cache with name: {cache_name} already exists.") 18 | elif isinstance(create_cache_response, CreateCache.Error): 19 | raise create_cache_response.inner_exception 20 | 21 | print("Setting Key: key to Value: value") 22 | set_response = cache_client.set(cache_name, "key", "value") 23 | 24 | if isinstance(set_response, CacheSet.Error): 25 | raise set_response.inner_exception 26 | 27 | print("Getting Key: key") 28 | get_response = cache_client.get(cache_name, "key") 29 | 30 | if isinstance(get_response, CacheGet.Hit): 31 | print(f"Look up resulted in a hit: {get_response}") 32 | print(f"Looked up Value: {get_response.value_string!r}") 33 | elif isinstance(get_response, CacheGet.Miss): 34 | print("Look up resulted in a: miss. This is unexpected.") 35 | elif isinstance(get_response, CacheGet.Error): 36 | raise get_response.inner_exception 37 | -------------------------------------------------------------------------------- /src/momento/responses/data/set/fetch.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | 6 | from ...mixins import ErrorResponseMixin 7 | from ...response import CacheResponse 8 | 9 | 10 | class CacheSetFetchResponse(CacheResponse): 11 | """Parent response type for a `set_fetch` request. 12 | 13 | Its subtypes are: 14 | - `CacheSetFetch.Hit` 15 | - `CacheSetFetch.Miss` 16 | - `CacheSetFetch.Error` 17 | 18 | See `CacheClient` for how to work with responses. 19 | """ 20 | 21 | 22 | class CacheSetFetch(ABC): 23 | """Groups all `CacheSetFetchResponse` derived types under a common namespace.""" 24 | 25 | @dataclass 26 | class Hit(CacheSetFetchResponse): 27 | """Indicates the set exists and its values were fetched.""" 28 | 29 | value_set_bytes: set[bytes] 30 | """The elements as a Python set. 31 | 32 | Use value_set_string to get the elements as a set. 33 | """ 34 | 35 | @property 36 | def value_set_string(self) -> set[str]: 37 | """The elements of the set, as utf-8 encoded strings. 38 | 39 | Returns: 40 | TSetElementsOutputStr 41 | """ 42 | return {v.decode("utf-8") for v in self.value_set_bytes} 43 | 44 | class Miss(CacheSetFetchResponse): 45 | """Indicates the set does not exist.""" 46 | 47 | class Error(CacheSetFetchResponse, ErrorResponseMixin): 48 | """Indicates an error occured in the request. 49 | 50 | This includes: 51 | - `error_code`: `MomentoErrorCode` value for the error. 52 | - `messsage`: a detailed error message. 53 | """ 54 | -------------------------------------------------------------------------------- /src/momento/responses/mixins.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | # https://stackoverflow.com/questions/71889556/mypy-checking-typing-protocol-with-python-3-7-support 4 | if typing.TYPE_CHECKING: 5 | from typing_extensions import Protocol 6 | else: 7 | Protocol = object 8 | 9 | from momento.errors import MomentoErrorCode, SdkException 10 | 11 | 12 | class ValueStringMixin: 13 | """Renders `value_bytes` as a utf-8 string. 14 | 15 | Returns: 16 | str: the utf-8 encoding of the data 17 | """ 18 | 19 | value_bytes: bytes 20 | 21 | @property 22 | def value_string(self) -> str: 23 | """Convert the bytes `value` to a UTF-8 string. 24 | 25 | Returns: 26 | str: UTF-8 representation of the `value` 27 | """ 28 | return self.value_bytes.decode("utf-8") 29 | 30 | 31 | class ErrorResponseMixin: 32 | _error: SdkException 33 | 34 | def __init__(self, _error: SdkException): 35 | self._error = _error 36 | 37 | @property 38 | def inner_exception(self) -> SdkException: 39 | """The SdkException object used to construct the response.""" 40 | return self._error 41 | 42 | @property 43 | def error_code(self) -> MomentoErrorCode: 44 | """The `MomentoErrorCode` value for the particular error object.""" 45 | return self._error.error_code 46 | 47 | @property 48 | def message(self) -> str: 49 | """An explanation of conditions that caused and potential ways to resolve the error.""" 50 | return f"{self._error.message_wrapper}: {self._error.message}" 51 | 52 | def __str__(self) -> str: 53 | return f"{self.__class__.__qualname__} {self.error_code}: {self.message}" 54 | -------------------------------------------------------------------------------- /examples/lambda/docker/lambda/index.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from momento import CacheClient, Configurations, CredentialProvider 4 | from momento.responses import CacheGet, CacheSet, CreateCache 5 | 6 | 7 | def handler(event, lambda_context): 8 | cache_name = "default-cache" 9 | with CacheClient.create( 10 | configuration=Configurations.Lambda.latest(), 11 | credential_provider=CredentialProvider.from_environment_variable("MOMENTO_API_KEY"), 12 | default_ttl=timedelta(seconds=60), 13 | ) as cache_client: 14 | create_cache_response = cache_client.create_cache(cache_name) 15 | 16 | if isinstance(create_cache_response, CreateCache.CacheAlreadyExists): 17 | print(f"Cache with name: {cache_name} already exists.") 18 | elif isinstance(create_cache_response, CreateCache.Error): 19 | raise create_cache_response.inner_exception 20 | 21 | print("Setting Key: key to Value: value") 22 | set_response = cache_client.set(cache_name, "key", "value") 23 | 24 | if isinstance(set_response, CacheSet.Error): 25 | raise set_response.inner_exception 26 | 27 | print("Getting Key: key") 28 | get_response = cache_client.get(cache_name, "key") 29 | 30 | if isinstance(get_response, CacheGet.Hit): 31 | print(f"Look up resulted in a hit: {get_response}") 32 | print(f"Looked up Value: {get_response.value_string!r}") 33 | elif isinstance(get_response, CacheGet.Miss): 34 | print("Look up resulted in a: miss. This is unexpected.") 35 | elif isinstance(get_response, CacheGet.Error): 36 | raise get_response.inner_exception 37 | -------------------------------------------------------------------------------- /examples/py310/quickstart.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from momento import CacheClient, Configurations, CredentialProvider 4 | from momento.responses import CacheGet, CacheSet, CreateCache 5 | 6 | if __name__ == "__main__": 7 | cache_name = "default-cache" 8 | with CacheClient.create( 9 | configuration=Configurations.Laptop.v1(), 10 | credential_provider=CredentialProvider.from_environment_variable("MOMENTO_API_KEY"), 11 | default_ttl=timedelta(seconds=60), 12 | ) as cache_client: 13 | create_cache_response = cache_client.create_cache(cache_name) 14 | match create_cache_response: 15 | case CreateCache.CacheAlreadyExists(): 16 | print(f"Cache with name: {cache_name} already exists.") 17 | case CreateCache.Error() as create_cache_error: 18 | raise create_cache_error.inner_exception 19 | 20 | print("Setting Key: foo to Value: FOO") 21 | set_response = cache_client.set(cache_name, "foo", "FOO") 22 | match set_response: 23 | case CacheSet.Error() as cache_set_error: 24 | raise cache_set_error.inner_exception 25 | 26 | print("Getting Key: foo") 27 | get_response = cache_client.get(cache_name, "foo") 28 | match get_response: 29 | case CacheGet.Hit() as hit: 30 | print(f"Look up resulted in a hit: {hit}") 31 | print(f"Looked up Value: {hit.value_string!r}") 32 | case CacheGet.Miss(): 33 | print("Look up resulted in a: miss. This is unexpected.") 34 | case CacheGet.Error() as cache_get_error: 35 | raise cache_get_error.inner_exception 36 | -------------------------------------------------------------------------------- /examples/py310/patterns.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import timedelta 3 | 4 | from momento import ( 5 | CacheClientAsync, 6 | Configurations, 7 | CredentialProvider, 8 | ) 9 | from momento.responses import ( 10 | CacheGet, 11 | CacheSet, 12 | ) 13 | 14 | database: dict[str, str] = {} 15 | database["test-key"] = "test-value" 16 | 17 | 18 | async def example_patterns_WriteThroughCaching(cache_client: CacheClientAsync): 19 | database.set("test-key", "test-value") 20 | set_response = await cache_client.set("test-cache", "test-key", "test-value") 21 | return 22 | 23 | 24 | # end example 25 | 26 | 27 | async def example_patterns_ReadAsideCaching(cache_client: CacheClientAsync): 28 | get_response = await cache_client.get("test-cache", "test-key") 29 | match get_response: 30 | case CacheGet.Hit() as hit: 31 | print(f"Retrieved value for key 'test-key': {hit.value_string}") 32 | return 33 | print(f"cache miss, fetching from database") 34 | actual_value = database.get("test-key") 35 | await cache_client.set("test-cache", "test-key", actual_value) 36 | return 37 | 38 | 39 | # end example 40 | 41 | 42 | async def main(): 43 | example_API_CredentialProviderFromEnvVar() 44 | 45 | await example_API_InstantiateCacheClient() 46 | cache_client = await CacheClientAsync.create( 47 | Configurations.Laptop.latest(), 48 | CredentialProvider.from_environment_variable("MOMENTO_API_KEY"), 49 | timedelta(seconds=60), 50 | ) 51 | 52 | await example_patterns_ReadAsideCaching(cache_client) 53 | await example_patterns_WriteThroughCaching(cache_client) 54 | 55 | 56 | if __name__ == "__main__": 57 | asyncio.run(main()) 58 | -------------------------------------------------------------------------------- /src/momento/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """Momento client error handling. 2 | 3 | The SDK error base class is `SdkException`, which is a subclass of `Exception`. 4 | Should a command raise an exception, the client will capture it return an `Error` response 5 | containing the exception details. 6 | """ 7 | 8 | from .error_details import ( 9 | MomentoErrorCode, 10 | MomentoErrorTransportDetails, 11 | MomentoGrpcErrorDetails, 12 | ) 13 | from .exceptions import ( 14 | AlreadyExistsException, 15 | AuthenticationException, 16 | BadRequestException, 17 | CancelledException, 18 | FailedPreconditionException, 19 | InternalServerException, 20 | InvalidArgumentException, 21 | LimitExceededException, 22 | NotFoundException, 23 | PermissionDeniedException, 24 | SdkException, 25 | ServerUnavailableException, 26 | TimeoutException, 27 | UnknownException, 28 | UnknownServiceException, 29 | ) 30 | 31 | # NB: since this module imports from sibling modules, it must be at the bottom 32 | # to avoid circular imports 33 | from .error_converter import convert_error # isort: skip 34 | 35 | __all__ = [ 36 | "MomentoErrorCode", 37 | "MomentoErrorTransportDetails", 38 | "MomentoGrpcErrorDetails", 39 | "AlreadyExistsException", 40 | "AuthenticationException", 41 | "BadRequestException", 42 | "CancelledException", 43 | "FailedPreconditionException", 44 | "InternalServerException", 45 | "InvalidArgumentException", 46 | "LimitExceededException", 47 | "NotFoundException", 48 | "PermissionDeniedException", 49 | "SdkException", 50 | "ServerUnavailableException", 51 | "TimeoutException", 52 | "UnknownException", 53 | "UnknownServiceException", 54 | "convert_error", 55 | ] 56 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/infrastructure.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/get_score.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin, ValueStringMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheSortedSetGetScoreResponse(CacheResponse): 9 | """Parent response type for a cache `sorted_set_get_score` request. 10 | 11 | Its subtypes are: 12 | - `CacheSortedSetGetScore.Hit` 13 | - `CacheSortedSetGetScore.Miss` 14 | - `CacheSortedSetGetScore.Error` 15 | 16 | See `CacheClient` for how to work with responses. 17 | """ 18 | 19 | 20 | class CacheSortedSetGetScore(ABC): 21 | """Groups all `CacheSortedSetGetScoreResponse` derived types under a common namespace.""" 22 | 23 | @dataclass 24 | class Hit(CacheSortedSetGetScoreResponse, ValueStringMixin): 25 | """Contains the result of a cache hit.""" 26 | 27 | value_bytes: bytes 28 | """The value for which the score was queried. Use the 29 | `value_string` property to access the value as a string.""" 30 | 31 | score: float 32 | """The score of the sorted set value that was queried.""" 33 | 34 | @dataclass 35 | class Miss(CacheSortedSetGetScoreResponse, ValueStringMixin): 36 | """Indicates the sorted set or sorted set element does not exist.""" 37 | 38 | value_bytes: bytes 39 | """The value that did not have a score. Use the 40 | `value_string` property to access the value as a string.""" 41 | 42 | class Error(CacheSortedSetGetScoreResponse, ErrorResponseMixin): 43 | """Contains information about an error returned from a request. 44 | 45 | This includes: 46 | - `error_code`: `MomentoErrorCode` value for the error. 47 | - `message`: a detailed error message. 48 | """ 49 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/fetch.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | 6 | from ...mixins import ErrorResponseMixin 7 | from ...response import CacheResponse 8 | 9 | 10 | class CacheSortedSetFetchResponse(CacheResponse): 11 | """Response type for a `sorted_set_fetch` request. 12 | 13 | Its subtypes are: 14 | - `CacheSortedSetFetch.Hit` 15 | - `CacheSortedSetFetch.Miss` 16 | - `CacheSortedSetFetch.Error` 17 | 18 | See `CacheClient` for how to work with responses. 19 | """ 20 | 21 | 22 | class CacheSortedSetFetch(ABC): 23 | """Groups all `CacheSortedSetFetchResponse` derived types under a common namespace.""" 24 | 25 | @dataclass 26 | class Hit(CacheSortedSetFetchResponse): 27 | """Indicates the sorted set exists and its values were fetched.""" 28 | 29 | value_list_bytes: list[tuple[bytes, float]] 30 | """The values for the fetched sorted set, as a list of bytes values to float score tuples.""" 31 | 32 | @property 33 | def value_list_string(self) -> list[tuple[str, float]]: 34 | """The values for the fetched sorted set, as a list of utf-8 encoded string values to float score tuples. 35 | 36 | Returns: 37 | list[tuple[str, float]] 38 | """ 39 | return [(key.decode("utf-8"), value) for key, value in self.value_list_bytes] 40 | 41 | class Miss(CacheSortedSetFetchResponse): 42 | """Indicates the sorted set does not exist.""" 43 | 44 | class Error(CacheSortedSetFetchResponse, ErrorResponseMixin): 45 | """Indicates an error occurred in the request. 46 | 47 | This includes: 48 | - `error_code`: `MomentoErrorCode` value for the error. 49 | - `message`: a detailed error message. 50 | """ 51 | -------------------------------------------------------------------------------- /examples/lambda/infrastructure/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "plugin:import/recommended", 11 | "plugin:prettier/recommended", 12 | "plugin:node/recommended" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 12, 17 | "project": "./tsconfig.json" 18 | }, 19 | "plugins": ["@typescript-eslint"], 20 | "rules": { 21 | "semi": ["error", "always"], 22 | "import/no-extraneous-dependencies": ["error", {}], 23 | "node/no-unsupported-features/es-syntax": "off", 24 | "node/no-missing-import": [ 25 | "error", 26 | { 27 | "tryExtensions": [".js", ".ts", ".json", ".node"] 28 | } 29 | ], 30 | "prettier/prettier": "error", 31 | "block-scoped-var": "error", 32 | "eqeqeq": "error", 33 | "no-var": "error", 34 | "prefer-const": "error", 35 | "eol-last": "error", 36 | "prefer-arrow-callback": "error", 37 | "no-trailing-spaces": "error", 38 | "quotes": ["warn", "single", {"avoidEscape": true}], 39 | "no-restricted-properties": [ 40 | "error", 41 | { 42 | "object": "describe", 43 | "property": "only" 44 | }, 45 | { 46 | "object": "it", 47 | "property": "only" 48 | } 49 | ], 50 | // async without await is often an error and in other uses it obfuscates 51 | // the intent of the developer. Functions are async when they want to await. 52 | "require-await": "error", 53 | "import/no-duplicates": "error" 54 | }, 55 | "settings": { 56 | "import/resolver": { 57 | "node": { 58 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/prepy310/topic_subscribe.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | 4 | from momento import ( 5 | CacheClient, 6 | Configurations, 7 | CredentialProvider, 8 | TopicClient, 9 | TopicConfigurations, 10 | ) 11 | from momento.responses import CreateCache, TopicSubscribe, TopicSubscriptionItem 12 | 13 | from example_utils.example_logging import initialize_logging 14 | 15 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 16 | _CACHE_NAME = "cache" 17 | _logger = logging.getLogger("topic-subscribe-example") 18 | 19 | 20 | def setup_cache() -> None: 21 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 22 | response = client.create_cache(_CACHE_NAME) 23 | if isinstance(response, CreateCache.Error): 24 | raise response.inner_exception 25 | 26 | 27 | def main() -> None: 28 | initialize_logging() 29 | setup_cache() 30 | _logger.info("hello") 31 | with TopicClient(TopicConfigurations.Default.v1(), _AUTH_PROVIDER) as client: 32 | subscription = client.subscribe("cache", "my_topic") 33 | if isinstance(subscription, TopicSubscribe.Error): 34 | raise Exception("got subscription error: ", subscription.message) 35 | 36 | if isinstance(subscription, TopicSubscribe.Subscription): 37 | print("polling for items. . .") 38 | for item in subscription: 39 | if isinstance(item, TopicSubscriptionItem.Text): 40 | print(f"got item as string: {item.value}") 41 | elif isinstance(item, TopicSubscriptionItem.Binary): 42 | print(f"got item as bytes: {item.value!r}") 43 | elif isinstance(item, TopicSubscriptionItem.Error): 44 | print(f"got item error: {item.message}") 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/get_field.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | 4 | from ...mixins import ErrorResponseMixin, ValueStringMixin 5 | from ...response import CacheResponse 6 | 7 | 8 | class CacheDictionaryGetFieldResponse(CacheResponse): 9 | """Parent response type for a cache `dictionary_get_field` request. 10 | 11 | Its subtypes are: 12 | - `CacheDictionaryGetField.Hit` 13 | - `CacheDictionaryGetField.Miss` 14 | - `CacheDictionaryGetField.Error` 15 | 16 | See `CacheClient` for how to work with responses. 17 | """ 18 | 19 | 20 | class CacheDictionaryGetField(ABC): 21 | """Groups all `CacheDictionaryGetFieldResponse` derived types under a common namespace.""" 22 | 23 | @dataclass 24 | class Hit(CacheDictionaryGetFieldResponse, ValueStringMixin): 25 | """Contains the result of a cache hit.""" 26 | 27 | value_bytes: bytes 28 | """The value returned from the cache for the specified field. Use the 29 | `value_string` property to access the value as a string.""" 30 | 31 | field_bytes: bytes 32 | """The dictionary field that was queried.""" 33 | 34 | @property 35 | def field_string(self) -> str: 36 | """Convert the bytes `field` to a UTF-8 string. 37 | 38 | Returns: 39 | str: UTF-8 representation of the `field` 40 | """ 41 | return self.field_bytes.decode("utf-8") 42 | 43 | class Miss(CacheDictionaryGetFieldResponse): 44 | """Indicates the dictionary or dictionary field does not exist.""" 45 | 46 | class Error(CacheDictionaryGetFieldResponse, ErrorResponseMixin): 47 | """Contains information about an error returned from a request. 48 | 49 | This includes: 50 | - `error_code`: `MomentoErrorCode` value for the error. 51 | - `messsage`: a detailed error message. 52 | """ 53 | -------------------------------------------------------------------------------- /examples/py310/topic_subscribe.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | 4 | from momento import ( 5 | CacheClient, 6 | Configurations, 7 | CredentialProvider, 8 | TopicClient, 9 | TopicConfigurations, 10 | ) 11 | from momento.responses import CreateCache, TopicSubscribe, TopicSubscriptionItem 12 | 13 | from example_utils.example_logging import initialize_logging 14 | 15 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 16 | _CACHE_NAME = "cache" 17 | _logger = logging.getLogger("topic-subscribe-example") 18 | 19 | 20 | def setup_cache() -> None: 21 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 22 | response = client.create_cache(_CACHE_NAME) 23 | match response: 24 | case CreateCache.Error(): 25 | raise response.inner_exception 26 | 27 | 28 | def main() -> None: 29 | initialize_logging() 30 | setup_cache() 31 | _logger.info("hello") 32 | with TopicClient(TopicConfigurations.Default.v1(), _AUTH_PROVIDER) as client: 33 | subscription = client.subscribe("cache", "my_topic") 34 | match subscription: 35 | case TopicSubscribe.Error(): 36 | raise Exception("got subscription error: ", subscription.message) 37 | case TopicSubscribe.Subscription(): 38 | print("polling for items. . .") 39 | for item in subscription: 40 | match item: 41 | case TopicSubscriptionItem.Text(): 42 | print(f"got item as string: {item.value}") 43 | case TopicSubscriptionItem.Binary(): 44 | print(f"got item as bytes: {item.value!r}") 45 | case TopicSubscriptionItem.Error(): 46 | print(f"got item error: {item.message}") 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /src/momento/typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Iterable, List, Mapping, Set, Union 4 | 5 | TCacheName = str 6 | TTopicName = str 7 | 8 | TMomentoValue = Union[str, bytes] 9 | 10 | # Scalar Types 11 | TScalarKey = Union[str, bytes] 12 | TScalarValue = TMomentoValue 13 | 14 | # Collections 15 | TCollectionName = str 16 | TCollectionValue = Union[str, bytes] 17 | 18 | # Dictionary Types 19 | TDictionaryName = TCollectionName 20 | TDictionaryField = Union[str, bytes] 21 | TDictionaryValue = TMomentoValue 22 | TDictionaryFields = Iterable[TDictionaryField] 23 | TDictionaryItems = Union[ 24 | Mapping[TDictionaryField, TDictionaryValue], 25 | # Mapping isn't covariant so we have to list out the types here 26 | Mapping[bytes, bytes], 27 | Mapping[bytes, str], 28 | Mapping[str, bytes], 29 | Mapping[str, str], 30 | ] 31 | 32 | # List Types 33 | TListName = TCollectionName 34 | TListValue = TMomentoValue 35 | TListValuesInputBytes = Iterable[bytes] 36 | TListValuesInputStr = Iterable[str] 37 | TListValuesInput = Iterable[TListValue] 38 | TListValuesOutputBytes = List[bytes] 39 | TListValuesOutputStr = List[str] 40 | TLIstValuesOutput = Union[TListValuesOutputBytes, TListValuesOutputStr] 41 | 42 | # Set Types 43 | TSetName = TCollectionName 44 | TSetElement = TMomentoValue 45 | TSetElementsInputBytes = Iterable[bytes] 46 | TSetElementsInputStr = Iterable[str] 47 | TSetElementsInput = Iterable[TSetElement] 48 | TSetElementsOutputStr = Set[str] 49 | TSetElementsOutputBytes = Set[bytes] 50 | TSetElementsOutput = Union[TSetElementsOutputBytes, TSetElementsOutputStr] 51 | 52 | # Sorted Set Types 53 | TSortedSetName = TCollectionName 54 | TSortedSetValue = TMomentoValue 55 | TSortedSetScore = float 56 | TSortedSetValues = Iterable[TSortedSetValue] 57 | TSortedSetElements = Union[ 58 | Mapping[TSortedSetValue, TSortedSetScore], 59 | # Mapping isn't covariant, so we have to list out the types here 60 | Mapping[bytes, float], 61 | Mapping[str, float], 62 | ] 63 | -------------------------------------------------------------------------------- /src/momento/responses/control/signing_key/create.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from abc import ABC 5 | from dataclasses import dataclass 6 | from datetime import datetime 7 | from typing import Any 8 | 9 | from momento.responses.response import ControlResponse 10 | 11 | from ...mixins import ErrorResponseMixin 12 | 13 | 14 | class CreateSigningKeyResponse(ControlResponse): 15 | """Parent response type for a cache `create_signing_key` request. 16 | 17 | Its subtypes are: 18 | - `CreateSigningKey.Success` 19 | - `CreateSigningKey.Error` 20 | 21 | See `CacheClient` for how to work with responses. 22 | """ 23 | 24 | 25 | class CreateSigningKey(ABC): 26 | """Groups all `CreateSigningKeyResponse` derived types under a common namespace.""" 27 | 28 | @dataclass 29 | class Success(CreateSigningKeyResponse): 30 | """The response from creating a signing key.""" 31 | 32 | key_id: str 33 | """The ID of the signing key""" 34 | endpoint: str 35 | """The endpoint of the signing key""" 36 | key: str 37 | """The signing key as a JSON string""" 38 | expires_at: datetime 39 | """When the key expires""" 40 | 41 | @staticmethod 42 | def from_grpc_response(grpc_create_signing_key_response: Any, endpoint: str) -> CreateSigningKey.Success: # type: ignore[misc] # noqa: E501 43 | key: str = grpc_create_signing_key_response.key 44 | key_id: str = json.loads(key)["kid"] 45 | expires_at: datetime = datetime.fromtimestamp(grpc_create_signing_key_response.expires_at) 46 | return CreateSigningKey.Success(key_id, endpoint, key, expires_at) 47 | 48 | class Error(CreateSigningKeyResponse, ErrorResponseMixin): 49 | """Contains information about an error returned from a request. 50 | 51 | This includes: 52 | - `error_code`: `MomentoErrorCode` value for the error. 53 | - `messsage`: a detailed error message. 54 | """ 55 | -------------------------------------------------------------------------------- /src/momento/responses/control/cache/create.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from momento.responses.response import ControlResponse 4 | 5 | from ...mixins import ErrorResponseMixin 6 | 7 | 8 | class CreateCacheResponse(ControlResponse): 9 | """Parent response type for a create cache request. 10 | 11 | The response object is resolved to a type-safe object of one of 12 | the following subtypes: 13 | - `CreateCache.Success` 14 | - `CreateCache.CacheAlreadyExists` 15 | - `CreateCache.Error` 16 | 17 | Pattern matching can be used to operate on the appropriate subtype. 18 | For example, in python 3.10+:: 19 | 20 | match response: 21 | case CreateCache.Success(): 22 | ... 23 | case CreateCache.CacheAlreadyExists(): 24 | ... 25 | case CreateCache.Error(): 26 | ... 27 | case _: 28 | # Shouldn't happen 29 | 30 | or equivalently in earlier versions of python:: 31 | 32 | if isinstance(response, CreateCache.Success): 33 | ... 34 | elif isinstance(response, CreateCache.AlreadyExists): 35 | ... 36 | elif isinstance(response, CreateCache.Error): 37 | ... 38 | else: 39 | # Shouldn't happen 40 | """ 41 | 42 | 43 | class CreateCache(ABC): 44 | """Groups all `CreateCacheResponse` derived types under a common namespace.""" 45 | 46 | class Success(CreateCacheResponse): 47 | """Indicates the request was successful.""" 48 | 49 | class CacheAlreadyExists(CreateCacheResponse): 50 | """Indicates that a cache with the requested name has already been created in the requesting account.""" 51 | 52 | class Error(CreateCacheResponse, ErrorResponseMixin): 53 | """Contains information about an error returned from a request. 54 | 55 | This includes: 56 | - `error_code`: `MomentoErrorCode` value for the error. 57 | - `messsage`: a detailed error message. 58 | """ 59 | -------------------------------------------------------------------------------- /src/momento/auth/access_control/disposable_token_scope.py: -------------------------------------------------------------------------------- 1 | """Disposable Token Scope. 2 | 3 | Defines the data classes for specifying the permission scope of a disposable token. 4 | """ 5 | from dataclasses import dataclass 6 | from typing import List, Optional, Union 7 | 8 | from momento.auth.access_control.permission_scope import ( 9 | CachePermission, 10 | Permissions, 11 | ) 12 | 13 | 14 | @dataclass 15 | class AllCacheItems: 16 | """Indicates permission to access all items in a cache.""" 17 | 18 | pass 19 | 20 | 21 | @dataclass 22 | class CacheItemKey: 23 | """The key of a cache item.""" 24 | 25 | key: str 26 | 27 | 28 | @dataclass 29 | class CacheItemKeyPrefix: 30 | """The prefix of a cache item key.""" 31 | 32 | key_prefix: str 33 | 34 | 35 | @dataclass 36 | class CacheItemSelector: 37 | """A selection of cache items to grant permissions to, either all cache items, a specific cache item, or a items that match a specified key prefix.""" 38 | 39 | cache_item: Union[CacheItemKey, CacheItemKeyPrefix, AllCacheItems, str] 40 | 41 | def is_all_cache_items(self) -> bool: 42 | return isinstance(self.cache_item, AllCacheItems) 43 | 44 | 45 | @dataclass 46 | class DisposableTokenCachePermission(CachePermission): 47 | """Encapsulates the information needed to grant permissions to a cache item.""" 48 | 49 | cache_item_selector: CacheItemSelector 50 | 51 | 52 | @dataclass 53 | class DisposableTokenCachePermissions: 54 | """A list of permissions to grant to a disposable token.""" 55 | 56 | disposable_token_permissions: List[DisposableTokenCachePermission] 57 | 58 | 59 | @dataclass 60 | class DisposableTokenScope: 61 | """A set of permissions to grant to a disposable token.""" 62 | 63 | disposable_token_scope: Union[Permissions, DisposableTokenCachePermissions] 64 | 65 | 66 | @dataclass 67 | class DisposableTokenProps: 68 | """Additional properties for a disposable token, such as token_id, which can be used to identify the source of a token.""" 69 | 70 | token_id: Optional[str] 71 | -------------------------------------------------------------------------------- /src/momento/responses/pubsub/subscription_item.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from warnings import warn 4 | 5 | from ..mixins import ErrorResponseMixin, ValueStringMixin 6 | from ..response import PubsubResponse 7 | 8 | 9 | class TopicSubscriptionItemResponse(PubsubResponse): 10 | """Parent response type for a topic subscription's `item` request. 11 | 12 | Its subtypes are: 13 | - `TopicSubscriptionItem.Text` 14 | - `TopicSubscriptionItem.Binary` 15 | - `TopicSubscriptionItem.Error` 16 | 17 | The subtype `TopicSubscriptionItem.Success` is deprecated. 18 | """ 19 | 20 | 21 | class TopicSubscriptionItem(ABC): 22 | """Groups all `TopicSubscriptionItemResponse` derived types under a common namespace.""" 23 | 24 | @dataclass 25 | class Success(TopicSubscriptionItemResponse, ValueStringMixin): 26 | """Indicates the request was successful.""" 27 | 28 | value_bytes: bytes 29 | """The item returned from the subscription for the specified topic. Use the 30 | `value_string` property to access the value as a string.""" 31 | 32 | def __post_init__(self) -> None: 33 | warn("Success is a deprecated response, use Text or Binary instead", DeprecationWarning, stacklevel=2) 34 | 35 | @dataclass 36 | class Text(TopicSubscriptionItemResponse): 37 | """Indicates the request was successful and value will be returned as a string.""" 38 | 39 | value: str 40 | sequence_number: int 41 | sequence_page: int 42 | 43 | @dataclass 44 | class Binary(TopicSubscriptionItemResponse): 45 | """Indicates the request was successful and value will be returned as bytes.""" 46 | 47 | value: bytes 48 | sequence_number: int 49 | sequence_page: int 50 | 51 | class Error(TopicSubscriptionItemResponse, ErrorResponseMixin): 52 | """Contains information about an error returned from a request. 53 | 54 | This includes: 55 | - `error_code`: `MomentoErrorCode` value for the error. 56 | - `messsage`: a detailed error message. 57 | """ 58 | -------------------------------------------------------------------------------- /examples/observability.py: -------------------------------------------------------------------------------- 1 | from utils.instrumentation import example_observability_setup_tracing 2 | 3 | from datetime import timedelta 4 | 5 | from momento import CacheClient, Configurations, CredentialProvider 6 | from momento.responses import CacheGet, CacheSet, CreateCache 7 | 8 | 9 | example_observability_setup_tracing() 10 | 11 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 12 | _ITEM_DEFAULT_TTL_SECONDS = timedelta(seconds=60) 13 | _CACHE_NAME = "test-cache" 14 | _KEY = "test-key" 15 | _VALUE = "test-value" 16 | 17 | 18 | def _create_cache(cache_client: CacheClient) -> None: 19 | create_cache_response = cache_client.create_cache(_CACHE_NAME) 20 | match create_cache_response: 21 | case CreateCache.Success(): 22 | pass 23 | case CreateCache.CacheAlreadyExists(): 24 | print(f"Cache with name: {_CACHE_NAME!r} already exists.") 25 | case CreateCache.Error() as error: 26 | print(f"Error creating cache: {error.message}") 27 | case _: 28 | print("Unreachable") 29 | 30 | 31 | def _set_cache(cache_client: CacheClient) -> None: 32 | set_cache_response = cache_client.set(_CACHE_NAME, _KEY, _VALUE) 33 | match set_cache_response: 34 | case CacheSet.Success(): 35 | pass 36 | case CacheSet.Error() as error: 37 | print(f"Error setting value: {error.message}") 38 | case _: 39 | print("Unreachable") 40 | 41 | 42 | def _get_cache(cache_client: CacheClient) -> None: 43 | get_cache_response = cache_client.get(_CACHE_NAME, _KEY) 44 | match get_cache_response: 45 | case CacheGet.Hit(): 46 | print("Value is " + get_cache_response.value_string) 47 | pass 48 | case CacheGet.Error() as error: 49 | print(f"Error setting value: {error.message}") 50 | case _: 51 | print("Unreachable") 52 | 53 | 54 | def main() -> None: 55 | with CacheClient(Configurations.Laptop.v1(), _AUTH_PROVIDER, _ITEM_DEFAULT_TTL_SECONDS) as cache_client: 56 | _create_cache(cache_client) 57 | _set_cache(cache_client) 58 | _get_cache(cache_client) 59 | 60 | 61 | main() 62 | print("Success! Zipkin at http://localhost:9411 should contain traces for the cache creation, get, and set.") 63 | -------------------------------------------------------------------------------- /src/momento/responses/data/dictionary/fetch.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | 6 | from ...mixins import ErrorResponseMixin 7 | from ...response import CacheResponse 8 | 9 | 10 | class CacheDictionaryFetchResponse(CacheResponse): 11 | """Response type for a `dictionary_fetch` request. 12 | 13 | Its subtypes are: 14 | - `CacheDictionaryFetch.Hit` 15 | - `CacheDictionaryFetch.Miss` 16 | - `CacheDictionaryFetch.Error` 17 | 18 | See `CacheClient` for how to work with responses. 19 | """ 20 | 21 | 22 | class CacheDictionaryFetch(ABC): 23 | """Groups all `CacheDictionaryFetchResponse` derived types under a common namespace.""" 24 | 25 | @dataclass 26 | class Hit(CacheDictionaryFetchResponse): 27 | """Indicates the dictionary exists and its items were fetched.""" 28 | 29 | value_dictionary_bytes_bytes: dict[bytes, bytes] 30 | """The items for the fetched dictionary, as a mapping from bytes to bytes. 31 | 32 | Returns: 33 | dict[bytes, bytes] 34 | """ 35 | 36 | @property 37 | def value_dictionary_string_bytes(self) -> dict[str, bytes]: 38 | """The items for the fetched dictionary, as a mapping from utf-8 encoded strings to bytes. 39 | 40 | Returns: 41 | dict[str, bytes] 42 | """ 43 | return {k.decode("utf-8"): v for k, v in self.value_dictionary_bytes_bytes.items()} 44 | 45 | @property 46 | def value_dictionary_string_string(self) -> dict[str, str]: 47 | """The items for the fetched dictionary, as a mapping from utf-8 encoded strings to utf-8 encoded strings. 48 | 49 | Returns: 50 | dict[str, str] 51 | """ 52 | return {k.decode("utf-8"): v.decode("utf-8") for k, v in self.value_dictionary_bytes_bytes.items()} 53 | 54 | class Miss(CacheDictionaryFetchResponse): 55 | """Indicates the dictionary does not exist.""" 56 | 57 | class Error(CacheDictionaryFetchResponse, ErrorResponseMixin): 58 | """Indicates an error occured in the request. 59 | 60 | This includes: 61 | - `error_code`: `MomentoErrorCode` value for the error. 62 | - `messsage`: a detailed error message. 63 | """ 64 | -------------------------------------------------------------------------------- /.github/workflows/on-push-to-main-branch.yml: -------------------------------------------------------------------------------- 1 | name: On push to main branch 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-24.04 10 | 11 | steps: 12 | - name: Setup repo 13 | uses: actions/checkout@v4 14 | with: 15 | token: ${{ secrets.MOMENTO_MACHINE_USER_GITHUB_TOKEN }} 16 | 17 | - name: Generate README 18 | uses: momentohq/standards-and-practices/github-actions/generate-and-commit-oss-readme@gh-actions-v2 19 | with: 20 | project_status: official 21 | project_stability: stable 22 | project_type: sdk 23 | sdk_language: Python 24 | dev_docs_slug: python 25 | template_file: ./README.template.md 26 | output_file: ./README.md 27 | 28 | release-please: 29 | needs: [build] 30 | runs-on: ubuntu-24.04 31 | outputs: 32 | release_created: ${{ steps.release.outputs.release_created }} 33 | tag_name: ${{ steps.release.outputs.tag_name }} 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - uses: googleapis/release-please-action@v4 39 | name: Release Please 40 | id: release 41 | with: 42 | token: ${{ secrets.MOMENTO_MACHINE_USER_GITHUB_TOKEN }} 43 | 44 | publish: 45 | needs: [release-please] 46 | if: ${{ needs.release-please.outputs.release_created == 'true' }} 47 | runs-on: ubuntu-24.04 48 | env: 49 | VERSION: ${{ needs.release-please.outputs.tag_name }} 50 | 51 | steps: 52 | - name: Setup repo 53 | uses: actions/checkout@v4 54 | 55 | - name: Install and configure Poetry 56 | uses: snok/install-poetry@v1 57 | with: 58 | version: 1.3.1 59 | virtualenvs-in-project: true 60 | 61 | - name: Setup Python 3.10 62 | uses: actions/setup-python@v4 63 | with: 64 | python-version: "3.10" 65 | 66 | - name: Build package 67 | run: poetry build 68 | 69 | - name: Publish package 70 | env: 71 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYTHON_CUSTOMER_SDK_PYPI_TOKEN }} 72 | run: | 73 | if [ -z "$VERSION" ] 74 | then 75 | echo "Unable to determine SDK version! Exiting!" 76 | exit 1 77 | fi 78 | 79 | echo "Going to publish version=$VERSION" 80 | poetry publish 81 | -------------------------------------------------------------------------------- /src/momento/retry/fixed_count_retry_strategy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | from .default_eligibility_strategy import DefaultEligibilityStrategy 5 | from .eligibility_strategy import EligibilityStrategy 6 | from .retry_strategy import RetryStrategy 7 | from .retryable_props import RetryableProps 8 | 9 | logger = logging.getLogger("fixed-count-retry-strategy") 10 | 11 | 12 | class FixedCountRetryStrategy(RetryStrategy): 13 | def __init__( 14 | self, *, max_attempts: int = 3, eligibility_strategy: DefaultEligibilityStrategy = DefaultEligibilityStrategy() 15 | ): 16 | self._eligibility_strategy: EligibilityStrategy = eligibility_strategy 17 | self._max_attempts: int = max_attempts 18 | 19 | def determine_when_to_retry(self, props: RetryableProps) -> Optional[float]: 20 | """Determines whether a grpc call can be retried and how long to wait before that retry. 21 | 22 | Args: 23 | props (RetryableProps): Information about the grpc call, its last invocation, and how many times the call 24 | has been made. 25 | 26 | :Returns 27 | The time in seconds before the next retry should occur or None if no retry should be attempted. 28 | """ 29 | if self._eligibility_strategy.is_eligible_for_retry(props) is False: 30 | logger.debug( 31 | "Request path: %s; retryable status code: %s. Request is not retryable.", 32 | props.grpc_method, 33 | props.grpc_status, # type: ignore[misc] 34 | ) 35 | return None 36 | 37 | if props.attempt_number > self._max_attempts: 38 | logger.debug( 39 | "Request path: %s; retryable status code: %s; number of attempts (%i) " 40 | "has exceeded max (%i); not retrying.", 41 | props.grpc_method, 42 | props.grpc_status, # type: ignore[misc] 43 | props.attempt_number, 44 | self._max_attempts, 45 | ) 46 | return None 47 | 48 | logger.debug( 49 | "Request path: %s; retryable status code: %s; number of attempts (%i) " "is not above max (%i); retrying.", 50 | props.grpc_method, 51 | props.grpc_status, # type: ignore[misc] 52 | props.attempt_number, 53 | self._max_attempts, 54 | ) 55 | return 0.0 56 | -------------------------------------------------------------------------------- /tests/momento/topic_client/shared_behaviors.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from momento.errors import MomentoErrorCode 4 | from momento.responses import PubsubResponse 5 | from momento.typing import TCacheName, TTopicName 6 | from typing_extensions import Protocol 7 | 8 | from tests.asserts import assert_response_is_error 9 | from tests.utils import uuid_str 10 | 11 | 12 | class TCacheNameValidator(Protocol): 13 | def __call__(self, cache_name: TCacheName) -> PubsubResponse: 14 | ... 15 | 16 | 17 | def a_cache_name_validator() -> None: 18 | def with_non_existent_cache_name_it_throws_not_found(cache_name_validator: TCacheNameValidator) -> None: 19 | cache_name = uuid_str() 20 | response = cache_name_validator(cache_name=cache_name) 21 | assert_response_is_error(response, error_code=MomentoErrorCode.NOT_FOUND_ERROR) 22 | 23 | def with_null_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: 24 | response = cache_name_validator(cache_name=None) # type: ignore 25 | assert_response_is_error( 26 | response, 27 | error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, 28 | inner_exception_message="Cache name must be a string", 29 | ) 30 | 31 | def with_empty_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: 32 | response = cache_name_validator(cache_name="") 33 | assert_response_is_error( 34 | response, 35 | error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, 36 | inner_exception_message="Cache name must not be empty", 37 | ) 38 | 39 | def with_bad_cache_name_throws_exception(cache_name_validator: TCacheNameValidator) -> None: 40 | response = cache_name_validator(cache_name=1) # type: ignore 41 | assert_response_is_error( 42 | response, 43 | error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, 44 | inner_exception_message="Cache name must be a string", 45 | ) 46 | 47 | 48 | class TTopicValidator(Protocol): 49 | def __call__(self, topic_name: TTopicName) -> PubsubResponse: 50 | ... 51 | 52 | 53 | def a_topic_validator() -> None: 54 | def with_null_topic_throws_exception(cache_name: str, topic_validator: TTopicValidator) -> None: 55 | response = topic_validator(topic_name=None) # type: ignore 56 | assert_response_is_error(response, error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR) 57 | -------------------------------------------------------------------------------- /tests/momento/topic_client/shared_behaviors_async.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Awaitable 4 | 5 | from momento.errors import MomentoErrorCode 6 | from momento.responses import PubsubResponse 7 | from momento.typing import TCacheName, TTopicName 8 | from typing_extensions import Protocol 9 | 10 | from tests.asserts import assert_response_is_error 11 | from tests.utils import uuid_str 12 | 13 | 14 | class TCacheNameValidator(Protocol): 15 | def __call__(self, cache_name: TCacheName) -> Awaitable[PubsubResponse]: 16 | ... 17 | 18 | 19 | def a_cache_name_validator() -> None: 20 | async def with_non_existent_cache_name_it_throws_not_found(cache_name_validator: TCacheNameValidator) -> None: 21 | cache_name = uuid_str() 22 | response = await cache_name_validator(cache_name=cache_name) 23 | assert_response_is_error(response, error_code=MomentoErrorCode.NOT_FOUND_ERROR) 24 | 25 | async def with_null_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: 26 | response = await cache_name_validator(cache_name=None) # type: ignore 27 | assert_response_is_error( 28 | response, 29 | error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, 30 | inner_exception_message="Cache name must be a string", 31 | ) 32 | 33 | async def with_empty_cache_name_it_throws_exception(cache_name_validator: TCacheNameValidator) -> None: 34 | response = await cache_name_validator(cache_name="") 35 | assert_response_is_error( 36 | response, 37 | error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, 38 | inner_exception_message="Cache name must not be empty", 39 | ) 40 | 41 | async def with_bad_cache_name_throws_exception(cache_name_validator: TCacheNameValidator) -> None: 42 | response = await cache_name_validator(cache_name=1) # type: ignore 43 | assert_response_is_error( 44 | response, 45 | error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR, 46 | inner_exception_message="Cache name must be a string", 47 | ) 48 | 49 | 50 | class TTopicValidator(Protocol): 51 | def __call__(self, topic_name: TTopicName) -> Awaitable[PubsubResponse]: 52 | ... 53 | 54 | 55 | def a_topic_validator() -> None: 56 | async def with_null_topic_throws_exception(cache_name: str, topic_validator: TTopicValidator) -> None: 57 | response = await topic_validator(topic_name=None) # type: ignore 58 | assert_response_is_error(response, error_code=MomentoErrorCode.INVALID_ARGUMENT_ERROR) 59 | -------------------------------------------------------------------------------- /src/momento/responses/control/cache/list.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | from typing import List 6 | 7 | from momento_wire_types import controlclient_pb2 as ctrl_pb 8 | 9 | from momento.responses.response import ControlResponse 10 | 11 | from ...mixins import ErrorResponseMixin 12 | 13 | 14 | class ListCachesResponse(ControlResponse): 15 | """Parent response type for a list caches request. 16 | 17 | The response object is resolved to a type-safe object of one of 18 | the following subtypes: 19 | - `ListCaches.Success` 20 | - `ListCaches.Error` 21 | 22 | Pattern matching can be used to operate on the appropriate subtype. 23 | For example, in python 3.10+:: 24 | 25 | match response: 26 | case ListCaches.Success(): 27 | ... 28 | case ListCaches.Error(): 29 | ... 30 | case _: 31 | # Shouldn't happen 32 | 33 | or equivalently in earlier versions of python:: 34 | 35 | if isinstance(response, ListCaches.Success): 36 | ... 37 | elif isinstance(response, ListCaches.Error): 38 | ... 39 | else: 40 | # Shouldn't happen 41 | """ 42 | 43 | 44 | @dataclass 45 | class CacheInfo: 46 | """Contains a Momento cache's name.""" 47 | 48 | name: str 49 | """Holds the name of the cache.""" 50 | 51 | 52 | class ListCaches(ABC): 53 | """Groups all `ListCachesResponse` derived types under a common namespace.""" 54 | 55 | @dataclass 56 | class Success(ListCachesResponse): 57 | """Indicates the request was successful.""" 58 | 59 | caches: List[CacheInfo] 60 | """The list of caches available to the user.""" 61 | 62 | @staticmethod 63 | def from_grpc_response(grpc_list_cache_response: ctrl_pb._ListCachesResponse) -> ListCaches.Success: 64 | """Initializes ListCacheResponse to handle list cache response. 65 | 66 | Args: 67 | grpc_list_cache_response: Protobuf based response returned by Scs. 68 | """ 69 | caches = [CacheInfo(cache.cache_name) for cache in grpc_list_cache_response.cache] # type: ignore[misc] 70 | return ListCaches.Success(caches=caches) 71 | 72 | class Error(ListCachesResponse, ErrorResponseMixin): 73 | """Contains information about an error returned from a request. 74 | 75 | This includes: 76 | - `error_code`: `MomentoErrorCode` value for the error. 77 | - `messsage`: a detailed error message. 78 | """ 79 | -------------------------------------------------------------------------------- /src/momento/responses/data/sorted_set/get_scores.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | 6 | from ...mixins import ErrorResponseMixin 7 | from ...response import CacheResponse 8 | from .get_score import CacheSortedSetGetScore, CacheSortedSetGetScoreResponse 9 | 10 | 11 | class CacheSortedSetGetScoresResponse(CacheResponse): 12 | """Parent response type for a cache `sorted_set_get_scores` request. 13 | 14 | Its subtypes are: 15 | - `CacheSortedSetGetScores.Hit` 16 | - `CacheSortedSetGetScores.Miss` 17 | - `CacheSortedSetGetScores.Error` 18 | 19 | See `CacheClient` for how to work with responses. 20 | """ 21 | 22 | 23 | class CacheSortedSetGetScores(ABC): 24 | """Groups all `CacheSortedSetGetScoresResponse` derived types under a common namespace.""" 25 | 26 | @dataclass 27 | class Hit(CacheSortedSetGetScoresResponse): 28 | """Contains the result of a cache hit.""" 29 | 30 | responses: list[CacheSortedSetGetScoreResponse] 31 | """The value returned from the cache for the specified field. Use the 32 | `value_string` property to access the value as a string.""" 33 | 34 | @property 35 | def value_dictionary_bytes(self) -> dict[bytes, float]: 36 | """The values for the fetched sorted set, as bytes to their float scores. 37 | 38 | Returns: 39 | dict[bytes, float] 40 | """ 41 | return { 42 | response.value_bytes: response.score 43 | for response in self.responses 44 | if isinstance(response, CacheSortedSetGetScore.Hit) 45 | } 46 | 47 | @property 48 | def value_dictionary_string(self) -> dict[str, float]: 49 | """The values for the fetched sorted set, as utf-8 encoded strings to their float scores. 50 | 51 | Returns: 52 | dict[str, float] 53 | """ 54 | return { 55 | response.value_string: response.score 56 | for response in self.responses 57 | if isinstance(response, CacheSortedSetGetScore.Hit) 58 | } 59 | 60 | class Miss(CacheSortedSetGetScoresResponse): 61 | """Indicates the sorted set does not exist.""" 62 | 63 | class Error(CacheSortedSetGetScoresResponse, ErrorResponseMixin): 64 | """Contains information about an error returned from a request. 65 | 66 | This includes: 67 | - `error_code`: `MomentoErrorCode` value for the error. 68 | - `message`: a detailed error message. 69 | """ 70 | -------------------------------------------------------------------------------- /tests/momento/local/momento_local_metrics_collector.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Dict, List 3 | 4 | from tests.momento.local.momento_rpc_method import MomentoRpcMethod 5 | 6 | 7 | class MomentoLocalMetricsCollector: 8 | def __init__(self) -> None: 9 | # Data structure to store timestamps: cacheName -> requestName -> [timestamps] 10 | self.data: Dict[str, Dict[MomentoRpcMethod, List[int]]] = defaultdict(lambda: defaultdict(list)) 11 | 12 | def add_timestamp(self, cache_name: str, request_name: MomentoRpcMethod, timestamp: int) -> None: 13 | """Add a timestamp for a specific request and cache. 14 | 15 | Args: 16 | cache_name: The name of the cache 17 | request_name: The name of the request (using MomentoRpcMethod enum) 18 | timestamp: The timestamp to record in seconds since epoch 19 | """ 20 | self.data[cache_name][request_name].append(timestamp) 21 | 22 | def get_total_retry_count(self, cache_name: str, request_name: MomentoRpcMethod) -> int: 23 | """Calculate the total retry count for a specific cache and request. 24 | 25 | Args: 26 | cache_name: The name of the cache 27 | request_name: The name of the request (using MomentoRpcMethod enum) 28 | 29 | Returns: 30 | The total number of retries 31 | """ 32 | timestamps = self.data.get(cache_name, {}).get(request_name, []) 33 | # Number of retries is one less than the number of timestamps 34 | return max(0, len(timestamps) - 1) 35 | 36 | def get_average_time_between_retries(self, cache_name: str, request_name: MomentoRpcMethod) -> float: 37 | """Calculate the average time between retries for a specific cache and request. 38 | 39 | Args: 40 | cache_name: The name of the cache 41 | request_name: The name of the request (using MomentoRpcMethod enum) 42 | 43 | Returns: 44 | The average time in seconds, or 0.0 if there are no retries 45 | """ 46 | timestamps = self.data.get(cache_name, {}).get(request_name, []) 47 | if len(timestamps) < 2: 48 | return 0.0 # No retries occurred 49 | 50 | total_interval = sum(timestamps[i] - timestamps[i - 1] for i in range(1, len(timestamps))) 51 | return total_interval / (len(timestamps) - 1) 52 | 53 | def get_all_metrics(self) -> Dict[str, Dict[MomentoRpcMethod, List[int]]]: 54 | """Retrieve all collected metrics for debugging or analysis. 55 | 56 | Returns: 57 | The complete data structure with all recorded metrics 58 | """ 59 | return self.data 60 | -------------------------------------------------------------------------------- /src/momento/errors/error_details.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from dataclasses import dataclass 3 | from typing import Optional 4 | 5 | import grpc 6 | 7 | 8 | @enum.unique 9 | class MomentoErrorCode(enum.Enum): 10 | """A list of all available Momento error codes. 11 | 12 | These can be used to check for specific types of errors on a failure response. 13 | """ 14 | 15 | INVALID_ARGUMENT_ERROR = (1,) 16 | """Invalid argument passed to Momento client""" 17 | UNKNOWN_SERVICE_ERROR = (2,) 18 | """Service returned an unknown response""" 19 | ALREADY_EXISTS_ERROR = (3,) 20 | """Cache with specified name already exists""" 21 | NOT_FOUND_ERROR = (4,) 22 | """Cache with specified name doesn't exist""" 23 | INTERNAL_SERVER_ERROR = (5,) 24 | """An unexpected error occurred while trying to fulfill the request""" 25 | PERMISSION_ERROR = (6,) 26 | """Insufficient permissions to perform operation""" 27 | AUTHENTICATION_ERROR = (7,) 28 | """Invalid authentication credentials to connect to cache service""" 29 | CANCELLED_ERROR = (8,) 30 | """Request was cancelled by the server""" 31 | LIMIT_EXCEEDED_ERROR = (9,) 32 | """Request rate exceeded the limits for the account""" 33 | BAD_REQUEST_ERROR = (10,) 34 | """Request was invalid""" 35 | TIMEOUT_ERROR = (11,) 36 | """Client's configured timeout was exceeded""" 37 | SERVER_UNAVAILABLE = (12,) 38 | """Server was unable to handle the request""" 39 | CLIENT_RESOURCE_EXHAUSTED = (13,) 40 | """A client resource most likely memory was exhausted""" 41 | FAILED_PRECONDITION_ERROR = (14,) 42 | """System is not in a state required for the operation's execution""" 43 | UNKNOWN_ERROR = 15 44 | """Unknown error has occurred""" 45 | CONNECTION_ERROR = 16 46 | """Connection to the Momento server failed""" 47 | 48 | 49 | @dataclass 50 | class MomentoGrpcErrorDetails: 51 | """Captures low-level information about an error, at the gRPC level. 52 | 53 | Hopefully this is only needed in rare cases, by Momento engineers, for debugging. 54 | """ 55 | 56 | code: grpc.StatusCode 57 | """The gRPC status code of the error repsonse""" 58 | details: str 59 | """Detailed information about the error""" 60 | metadata: Optional[grpc.aio.Metadata] = None 61 | """Headers and other information about the error response""" 62 | # TODO need to reconcile synchronous metadata (above) with grpc.aio.Metadata 63 | 64 | 65 | @dataclass 66 | class MomentoErrorTransportDetails: 67 | """Container for low-level error information, including details from the transport layer.""" 68 | 69 | grpc: MomentoGrpcErrorDetails 70 | """Low-level gRPC error details""" 71 | -------------------------------------------------------------------------------- /src/momento/internal/synchronous/_utilities.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import collections 4 | from typing import Optional, Tuple 5 | 6 | import grpc 7 | from grpc import CallCredentials 8 | from grpc._typing import MetadataType 9 | 10 | from momento.errors import InvalidArgumentException 11 | from momento.internal.services import Service 12 | 13 | 14 | def make_metadata(cache_name: str) -> list[Tuple[str, str]]: 15 | return [("cache", cache_name)] 16 | 17 | 18 | class _ClientCallDetails( 19 | collections.namedtuple("_ClientCallDetails", ("method", "timeout", "metadata", "credentials")), 20 | grpc.ClientCallDetails, 21 | ): 22 | def __new__( 23 | cls, method: str, timeout: Optional[float], metadata: MetadataType, credentials: Optional[CallCredentials] 24 | ) -> _ClientCallDetails: 25 | return super().__new__(cls, method, timeout, metadata, credentials) 26 | 27 | 28 | def sanitize_client_call_details(client_call_details: grpc.ClientCallDetails) -> grpc.ClientCallDetails: 29 | """Defensive function meant to handle inbound gRPC client request objects. 30 | 31 | Args: 32 | client_call_details: the original inbound client grpc request we are intercepting 33 | 34 | Returns: a new client_call_details object with metadata properly initialized to a `grpc.aio.Metadata` object 35 | """ 36 | # Makes sure we can handle properly when we inject our own metadata onto request object. 37 | # This was mainly done as temporary fix after we observed ddtrace grpc client interceptor passing 38 | # client_call_details.metadata as a list instead of a grpc.aio.Metadata object. 39 | # See this ticket for follow-up actions to come back in and address this longer term: 40 | # https://github.com/momentohq/client-sdk-python/issues/149 41 | # If no metadata set on passed in client call details then we are first to set, so we should just initialize 42 | if client_call_details.metadata is None: 43 | return _ClientCallDetails( 44 | method=client_call_details.method, 45 | timeout=client_call_details.timeout, 46 | metadata=[], 47 | credentials=client_call_details.credentials, 48 | ) 49 | 50 | # This is block hit when ddtrace interceptor runs first and sets metadata as a list 51 | elif isinstance(client_call_details.metadata, list): 52 | return client_call_details 53 | else: 54 | # Else we raise exception for now since we don't know how to handle an unknown type 55 | raise InvalidArgumentException( 56 | "unexpected grpc client request metadata property passed to interceptor " 57 | "type=" + str(type(client_call_details.metadata)), 58 | Service.AUTH, 59 | ) 60 | -------------------------------------------------------------------------------- /examples/py310/topic_subscribe_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from datetime import timedelta 4 | 5 | from momento import ( 6 | CacheClient, 7 | Configurations, 8 | CredentialProvider, 9 | TopicClientAsync, 10 | TopicConfigurations, 11 | ) 12 | from momento.errors import SdkException 13 | from momento.responses import CreateCache, TopicSubscribe, TopicSubscriptionItem 14 | 15 | from example_utils.example_logging import initialize_logging 16 | 17 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 18 | _CACHE_NAME = "cache" 19 | _NUM_SUBSCRIBERS = 10 20 | _logger = logging.getLogger("topic-subscribe-example") 21 | 22 | 23 | def setup_cache() -> None: 24 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 25 | response = client.create_cache(_CACHE_NAME) 26 | match response: 27 | case CreateCache.Error(): 28 | raise response.inner_exception 29 | 30 | 31 | async def main() -> None: 32 | initialize_logging() 33 | setup_cache() 34 | _logger.info("hello") 35 | async with TopicClientAsync( 36 | TopicConfigurations.Default.v1().with_max_subscriptions(_NUM_SUBSCRIBERS), _AUTH_PROVIDER 37 | ) as client: 38 | tasks = [] 39 | for i in range(0, _NUM_SUBSCRIBERS): 40 | subscription = await client.subscribe("cache", "my_topic") 41 | match subscription: 42 | case TopicSubscribe.SubscriptionAsync(): 43 | tasks.append(asyncio.create_task(poll_subscription(subscription))) 44 | case TopicSubscribe.Error(): 45 | print("got subscription error: ", subscription.message) 46 | 47 | if len(tasks) == 0: 48 | raise Exception("no subscriptions were successful") 49 | 50 | print(f"{len(tasks)} subscriptions polling for items. . .") 51 | try: 52 | await asyncio.gather(*tasks) 53 | except SdkException: 54 | print("got exception") 55 | for task in tasks: 56 | task.cancel() 57 | 58 | 59 | async def poll_subscription(subscription: TopicSubscribe.SubscriptionAsync): 60 | async for item in subscription: 61 | match item: 62 | case TopicSubscriptionItem.Text(): 63 | print(f"got item as string: {item.value}") 64 | case TopicSubscriptionItem.Binary(): 65 | print(f"got item as bytes: {item.value!r}") 66 | case TopicSubscriptionItem.Error(): 67 | print("stream closed") 68 | print(item.inner_exception.message) 69 | return 70 | 71 | 72 | if __name__ == "__main__": 73 | asyncio.run(main()) 74 | -------------------------------------------------------------------------------- /examples/prepy310/topic_subscribe_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from datetime import timedelta 4 | 5 | from momento import ( 6 | CacheClient, 7 | Configurations, 8 | CredentialProvider, 9 | TopicClientAsync, 10 | TopicConfigurations, 11 | ) 12 | from momento.errors import SdkException 13 | from momento.responses import CreateCache, TopicSubscribe, TopicSubscriptionItem 14 | 15 | from example_utils.example_logging import initialize_logging 16 | 17 | _AUTH_PROVIDER = CredentialProvider.from_environment_variable("MOMENTO_API_KEY") 18 | _CACHE_NAME = "cache" 19 | _NUM_SUBSCRIBERS = 10 20 | _logger = logging.getLogger("topic-subscribe-example") 21 | 22 | 23 | def setup_cache() -> None: 24 | with CacheClient.create(Configurations.Laptop.latest(), _AUTH_PROVIDER, timedelta(seconds=60)) as client: 25 | response = client.create_cache(_CACHE_NAME) 26 | if isinstance(response, CreateCache.Error): 27 | raise response.inner_exception 28 | 29 | 30 | async def main() -> None: 31 | initialize_logging() 32 | setup_cache() 33 | _logger.info("hello") 34 | async with TopicClientAsync( 35 | TopicConfigurations.Default.v1().with_max_subscriptions(_NUM_SUBSCRIBERS), _AUTH_PROVIDER 36 | ) as client: 37 | subscriptions = [] 38 | for i in range(0, _NUM_SUBSCRIBERS): 39 | subscription = await client.subscribe("cache", "my_topic") 40 | if isinstance(subscription, TopicSubscribe.SubscriptionAsync): 41 | subscriptions.append(subscription) 42 | elif isinstance(subscription, TopicSubscribe.Error): 43 | print("got subscription error: ", subscription.message) 44 | 45 | if len(subscriptions) == 0: 46 | raise Exception("no subscriptions were successful") 47 | 48 | print(f"{len(subscriptions)} subscriptions polling for items. . .", flush=True) 49 | tasks = [asyncio.create_task(poll_subscription(subscription)) for subscription in subscriptions] 50 | try: 51 | await asyncio.gather(*tasks) 52 | except SdkException: 53 | print("got exception") 54 | for task in tasks: 55 | task.cancel() 56 | 57 | 58 | async def poll_subscription(subscription: TopicSubscribe.SubscriptionAsync): 59 | async for item in subscription: 60 | if isinstance(item, TopicSubscriptionItem.Text): 61 | print(f"got item as string: {item.value}") 62 | elif isinstance(item, TopicSubscriptionItem.Binary): 63 | print(f"got item as bytes: {item.value!r}") 64 | elif isinstance(item, TopicSubscriptionItem.Error): 65 | print("stream closed") 66 | print(item.inner_exception.message) 67 | return 68 | 69 | 70 | if __name__ == "__main__": 71 | asyncio.run(main()) 72 | -------------------------------------------------------------------------------- /src/momento/config/topic_configuration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import timedelta 4 | 5 | from momento.config.transport.topic_transport_strategy import TopicTransportStrategy 6 | 7 | 8 | class TopicConfiguration: 9 | """Configuration options for Momento topic client.""" 10 | 11 | def __init__(self, transport_strategy: TopicTransportStrategy, max_subscriptions: int = 0): 12 | """Instantiate a configuration. 13 | 14 | Args: 15 | transport_strategy (TopicTransportStrategy): Configuration options for networking with 16 | the Momento pubsub service. 17 | max_subscriptions (int): The maximum number of subscriptions the client is expected 18 | to handle. Because each gRPC channel can handle 100 connections, we must explicitly 19 | open multiple channels to accommodate the load. NOTE: if the number of connection 20 | attempts exceeds the number the channels can support, program execution will block 21 | and hang. 22 | """ 23 | self._max_subscriptions = max_subscriptions 24 | self._transport_strategy = transport_strategy 25 | 26 | def get_max_subscriptions(self) -> int: 27 | return self._max_subscriptions 28 | 29 | def with_max_subscriptions(self, max_subscriptions: int) -> TopicConfiguration: 30 | return TopicConfiguration(self._transport_strategy, max_subscriptions) 31 | 32 | def get_transport_strategy(self) -> TopicTransportStrategy: 33 | """Access the transport strategy. 34 | 35 | Returns: 36 | TopicTransportStrategy: the current configuration options for wire interactions with the Momento pubsub service. 37 | """ 38 | return self._transport_strategy 39 | 40 | def with_transport_strategy(self, transport_strategy: TopicTransportStrategy) -> TopicConfiguration: 41 | """Copy constructor for overriding TopicTransportStrategy. 42 | 43 | Args: 44 | transport_strategy (TopicTransportStrategy): the new TopicTransportStrategy. 45 | 46 | Returns: 47 | TopicConfiguration: the new TopicConfiguration with the specified TopicTransportStrategy. 48 | """ 49 | return TopicConfiguration(transport_strategy, self._max_subscriptions) 50 | 51 | def with_client_timeout(self, client_timeout: timedelta) -> TopicConfiguration: 52 | """Copies the TopicConfiguration and sets the new client-side timeout in the copy's TopicTransportStrategy. 53 | 54 | Args: 55 | client_timeout (timedelta): the new client-side timeout. 56 | 57 | Return: 58 | TopicConfiguration: the new TopicConfiguration. 59 | """ 60 | return TopicConfiguration(self._transport_strategy.with_client_timeout(client_timeout), self._max_subscriptions) 61 | -------------------------------------------------------------------------------- /src/momento/responses/control/signing_key/list.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from dataclasses import dataclass 5 | from datetime import datetime 6 | from typing import Any 7 | 8 | import google 9 | 10 | from momento.responses.response import ControlResponse 11 | 12 | from ...mixins import ErrorResponseMixin 13 | 14 | 15 | @dataclass 16 | class SigningKey: 17 | """Signing keys returned from requesting list signing keys.""" 18 | 19 | key_id: str 20 | """the ID of the signing key""" 21 | expires_at: datetime 22 | """when the key expires""" 23 | endpoint: str 24 | """endpoint of the signing key""" 25 | 26 | @staticmethod 27 | def from_grpc_response(grpc_listed_signing_key: Any, endpoint: str) -> SigningKey: # type: ignore[misc] 28 | key_id: str = grpc_listed_signing_key.key_id 29 | expires_at: datetime = datetime.fromtimestamp(grpc_listed_signing_key.expires_at) 30 | return SigningKey(key_id, expires_at, endpoint) 31 | 32 | 33 | class ListSigningKeysResponse(ControlResponse): 34 | """Parent response type for a cache `list_signing_key` request. 35 | 36 | Its subtypes are: 37 | - `ListSigningKeys.Success` 38 | - `ListSigningKeys.Error` 39 | 40 | See `CacheClient` for how to work with responses. 41 | """ 42 | 43 | 44 | class ListSigningKeys(ABC): 45 | """Groups all `ListSigningKeysResponse` derived types under a common namespace.""" 46 | 47 | @dataclass 48 | class Success(ListSigningKeysResponse): 49 | """The response from listing signing keys.""" 50 | 51 | signing_keys: list[SigningKey] 52 | """all signing keys in this page""" 53 | 54 | @staticmethod 55 | def from_grpc_response( # type:ignore[misc] 56 | grpc_list_signing_keys_response: google.protobuf.message.Message, endpoint: str 57 | ) -> ListSigningKeys.Success: 58 | """Creates a ListSigningKeysResponse from a grpc response. 59 | 60 | Args: 61 | grpc_list_signing_keys_response (google.protobuf.message.Message): the grpc response 62 | endpoint (str): endpoint of the signing key 63 | 64 | Returns: 65 | ListSigningKeysResponse 66 | """ 67 | signing_keys: list[SigningKey] = [ 68 | SigningKey.from_grpc_response(signing_key, endpoint) 69 | for signing_key in grpc_list_signing_keys_response.signing_key 70 | ] 71 | return ListSigningKeys.Success(signing_keys) 72 | 73 | class Error(ListSigningKeysResponse, ErrorResponseMixin): 74 | """Contains information about an error returned from a request. 75 | 76 | This includes: 77 | - `error_code`: `MomentoErrorCode` value for the error. 78 | - `messsage`: a detailed error message. 79 | """ 80 | -------------------------------------------------------------------------------- /src/momento/config/auth_configuration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import timedelta 4 | 5 | from momento.config.transport.transport_strategy import TransportStrategy 6 | from momento.retry.retry_strategy import RetryStrategy 7 | 8 | 9 | class AuthConfiguration: 10 | """AuthConfiguration options for Momento Auth Client.""" 11 | 12 | def __init__(self, transport_strategy: TransportStrategy, retry_strategy: RetryStrategy): 13 | """Instantiate a AuthConfiguration. 14 | 15 | Args: 16 | transport_strategy (TransportStrategy): AuthConfiguration options for networking with 17 | the Momento service. 18 | retry_strategy (RetryStrategy): the strategy to use when determining whether to retry a grpc call. 19 | """ 20 | self._transport_strategy = transport_strategy 21 | self._retry_strategy = retry_strategy 22 | 23 | def get_retry_strategy(self) -> RetryStrategy: 24 | """Access the retry strategy. 25 | 26 | Returns: 27 | RetryStrategy: the strategy to use when determining whether to retry a grpc call. 28 | """ 29 | return self._retry_strategy 30 | 31 | def with_retry_strategy(self, retry_strategy: RetryStrategy) -> AuthConfiguration: 32 | """Copy constructor for overriding RetryStrategy. 33 | 34 | Args: 35 | retry_strategy (RetryStrategy): the new RetryStrategy. 36 | 37 | Returns: 38 | AuthConfiguration: the new AuthConfiguration with the specified RetryStrategy. 39 | """ 40 | return AuthConfiguration(self._transport_strategy, retry_strategy) 41 | 42 | def get_transport_strategy(self) -> TransportStrategy: 43 | """Access the transport strategy. 44 | 45 | Returns: 46 | TransportStrategy: the current configuration options for wire interactions with the Momento service. 47 | """ 48 | return self._transport_strategy 49 | 50 | def with_transport_strategy(self, transport_strategy: TransportStrategy) -> AuthConfiguration: 51 | """Copy constructor for overriding TransportStrategy. 52 | 53 | Args: 54 | transport_strategy (TransportStrategy): the new TransportStrategy. 55 | 56 | Returns: 57 | AuthConfiguration: the new AuthConfiguration with the specified TransportStrategy. 58 | """ 59 | return AuthConfiguration(transport_strategy, self._retry_strategy) 60 | 61 | def with_client_timeout(self, client_timeout: timedelta) -> AuthConfiguration: 62 | """Copies the AuthConfiguration and sets the new client-side timeout in the copy's TransportStrategy. 63 | 64 | Args: 65 | client_timeout (timedelta): the new client-side timeout. 66 | 67 | Return: 68 | AuthConfiguration: the new AuthConfiguration. 69 | """ 70 | return AuthConfiguration(self._transport_strategy.with_client_timeout(client_timeout), self._retry_strategy) 71 | -------------------------------------------------------------------------------- /tests/momento/requests/test_collection_ttl.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from typing import Optional 3 | 4 | import pytest 5 | from momento.errors import InvalidArgumentException 6 | from momento.requests import CollectionTtl 7 | 8 | 9 | def describe_collection_ttl() -> None: 10 | @pytest.mark.parametrize( 11 | "collection_ttl, expected_ttl, expected_refresh_ttl", 12 | [ 13 | (CollectionTtl(ttl=None, refresh_ttl=False), None, False), 14 | (CollectionTtl(ttl=timedelta(days=1), refresh_ttl=False), timedelta(days=1), False), 15 | (CollectionTtl.from_cache_ttl(), None, True), 16 | (CollectionTtl.of(ttl=timedelta(days=1)), timedelta(days=1), True), 17 | (CollectionTtl.refresh_ttl_if_provided(ttl=None), None, False), 18 | (CollectionTtl.refresh_ttl_if_provided(ttl=timedelta(days=1)), timedelta(days=1), True), 19 | ], 20 | ) 21 | def test_collection_ttl_builders( 22 | collection_ttl: CollectionTtl, expected_ttl: Optional[timedelta], expected_refresh_ttl: bool 23 | ) -> None: 24 | if expected_ttl is None: 25 | assert collection_ttl.ttl is None, "ttl was not None but should be" 26 | else: 27 | assert collection_ttl.ttl == expected_ttl, "ttl did not match" 28 | assert collection_ttl.refresh_ttl == expected_refresh_ttl, "refresh_ttl did not match" 29 | 30 | @pytest.mark.parametrize( 31 | "collection_ttl", 32 | [ 33 | (CollectionTtl(ttl=None, refresh_ttl=False)), 34 | (CollectionTtl(ttl=timedelta(days=1), refresh_ttl=False)), 35 | (CollectionTtl(ttl=None, refresh_ttl=True)), 36 | ], 37 | ) 38 | def test_with_and_without_refresh_ttl_on_updates(collection_ttl: CollectionTtl) -> None: 39 | new_collection_ttl = collection_ttl.with_refresh_ttl_on_updates() 40 | assert collection_ttl.ttl == new_collection_ttl.ttl, "ttl should have passed through identically but did not" 41 | assert new_collection_ttl.refresh_ttl, "refresh_ttl should be true after with_refresh_ttl_on_updates but wasn't" 42 | 43 | new_collection_ttl = collection_ttl.with_no_refresh_ttl_on_updates() 44 | assert collection_ttl.ttl == new_collection_ttl.ttl, "ttl should have passed through identically but did not" 45 | assert ( 46 | not new_collection_ttl.refresh_ttl 47 | ), "refresh_ttl should be false after with_no_refresh_ttl_on_updates but wasn't" 48 | 49 | @pytest.mark.parametrize( 50 | "ttl", 51 | [ 52 | (timedelta(seconds=-1)), 53 | (timedelta(seconds=42, minutes=-99)), 54 | ], 55 | ) 56 | def test_negative_ttl(ttl: timedelta) -> None: 57 | with pytest.raises(InvalidArgumentException, match=r"ttl must be a positive amount of time."): 58 | CollectionTtl(ttl=ttl) 59 | 60 | def test_int_ttl() -> None: 61 | with pytest.raises(InvalidArgumentException, match=r"ttl must be a timedelta."): 62 | CollectionTtl(ttl=23) # type: ignore[arg-type] 63 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main", release ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '27 23 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /src/momento/logs.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import warnings 3 | from typing import Optional 4 | 5 | logger = logging.getLogger("momentosdk") 6 | 7 | 8 | """ info('some %s stuff', 'information') """ 9 | info = logger.info 10 | 11 | 12 | """ debug('some %s stuff', 'debug') """ 13 | debug = logger.debug 14 | 15 | 16 | def add_logging_level(level_name: str, level_num: int, method_name: Optional[str] = None) -> bool: 17 | """Comprehensively adds a new logging level to the `logging` module and the currently configured logging class. 18 | 19 | `level_name` becomes an attribute of the `logging` module with the value 20 | `level_num`. `method_name` becomes a convenience method for both `logging` 21 | itself and the class returned by `logging.getLoggerClass()` (usually just 22 | `logging.Logger`). If `method_name` is not specified, `level_name.lower()` is 23 | used. 24 | 25 | To avoid accidental clobberings of existing attributes, this method will 26 | raise an `AttributeError` if the level name is already an attribute of the 27 | `logging` module or if the method name is already present. 28 | 29 | Example:: 30 | ------- 31 | >>> add_logging_level('TRACE', logging.DEBUG - 5) 32 | >>> logging.getLogger(__name__).setLevel("TRACE") 33 | >>> logging.getLogger(__name__).trace('that worked') 34 | >>> logging.trace('so did this') 35 | >>> logging.TRACE 36 | 5 37 | 38 | """ 39 | if not method_name: 40 | method_name = level_name.lower() 41 | 42 | if hasattr(logging, level_name) or hasattr(logging, method_name) or hasattr(logging.getLoggerClass(), method_name): 43 | # Do nothing if the logging level already exists. Since this affects logging globally we can't know if the user 44 | # or another dependency has already added the same level. 45 | warnings.warn( 46 | "Logging level {level} or logging method {method} already defined. Skipping.".format( 47 | level=level_name, method=method_name 48 | ), 49 | stacklevel=1, 50 | ) 51 | return False 52 | 53 | # This method was inspired by the answers to Stack Overflow post 54 | # http://stackoverflow.com/q/2183233/2988730, especially 55 | # http://stackoverflow.com/a/13638084/2988730 56 | def logForLevel(self, message, *args, **kwargs): # type: ignore[no-untyped-def] 57 | if self.isEnabledFor(level_num): # type: ignore[misc] 58 | self._log(level_num, message, args, **kwargs) # type: ignore[misc] 59 | 60 | def logToRoot(message, *args, **kwargs): # type: ignore[no-untyped-def] 61 | logging.log(level_num, message, *args, **kwargs) # type: ignore[misc] 62 | 63 | logging.addLevelName(level_num, level_name) 64 | setattr(logging, level_name, level_num) 65 | setattr(logging.getLoggerClass(), method_name, logForLevel) # type: ignore[misc] 66 | setattr(logging, method_name, logToRoot) # type: ignore[misc] 67 | 68 | return True 69 | 70 | 71 | TRACE = logging.DEBUG - 5 72 | 73 | 74 | def initialize_momento_logging() -> None: 75 | add_logging_level("TRACE", TRACE) 76 | -------------------------------------------------------------------------------- /tests/momento/local/test_fixed_count_retry_strategy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from momento.errors import MomentoErrorCode 3 | from momento.responses import CacheGet, CacheIncrement 4 | 5 | from tests.conftest import client_local 6 | from tests.momento.local.momento_local_async_middleware import MomentoLocalMiddlewareArgs 7 | from tests.momento.local.momento_local_metrics_collector import MomentoLocalMetricsCollector 8 | from tests.momento.local.momento_rpc_method import MomentoRpcMethod 9 | from tests.utils import uuid_str 10 | 11 | 12 | @pytest.mark.local 13 | def test_retry_eligible_api_should_make_max_attempts_when_full_network_outage() -> None: 14 | metrics_collector = MomentoLocalMetricsCollector() 15 | middleware_args = MomentoLocalMiddlewareArgs( 16 | request_id=str(uuid_str()), 17 | test_metrics_collector=metrics_collector, 18 | return_error=MomentoErrorCode.SERVER_UNAVAILABLE, 19 | error_rpc_list=[MomentoRpcMethod.GET], 20 | ) 21 | cache_name = uuid_str() 22 | 23 | with client_local(cache_name, middleware_args) as client: 24 | response = client.get(cache_name, "key") 25 | 26 | assert isinstance(response, CacheGet.Error) 27 | assert response.error_code == MomentoErrorCode.SERVER_UNAVAILABLE 28 | 29 | retry_count = metrics_collector.get_total_retry_count(cache_name, MomentoRpcMethod.GET) 30 | assert retry_count == 3 31 | 32 | 33 | @pytest.mark.local 34 | def test_non_retry_eligible_api_should_make_no_attempts_when_full_network_outage() -> None: 35 | metrics_collector = MomentoLocalMetricsCollector() 36 | middleware_args = MomentoLocalMiddlewareArgs( 37 | request_id=str(uuid_str()), 38 | test_metrics_collector=metrics_collector, 39 | return_error=MomentoErrorCode.SERVER_UNAVAILABLE, 40 | error_rpc_list=[MomentoRpcMethod.INCREMENT], 41 | ) 42 | cache_name = uuid_str() 43 | 44 | with client_local(cache_name, middleware_args) as client: 45 | response = client.increment(cache_name, "key", 1) 46 | 47 | assert isinstance(response, CacheIncrement.Error) 48 | assert response.error_code == MomentoErrorCode.SERVER_UNAVAILABLE 49 | 50 | retry_count = metrics_collector.get_total_retry_count(cache_name, MomentoRpcMethod.INCREMENT) 51 | assert retry_count == 0 52 | 53 | 54 | @pytest.mark.local 55 | def test_retry_eligible_api_should_make_less_than_max_attempts_when_temporary_network_outage() -> None: 56 | metrics_collector = MomentoLocalMetricsCollector() 57 | middleware_args = MomentoLocalMiddlewareArgs( 58 | request_id=str(uuid_str()), 59 | test_metrics_collector=metrics_collector, 60 | return_error=MomentoErrorCode.SERVER_UNAVAILABLE, 61 | error_rpc_list=[MomentoRpcMethod.GET], 62 | error_count=2, 63 | ) 64 | cache_name = uuid_str() 65 | 66 | with client_local(cache_name, middleware_args) as client: 67 | response = client.get(cache_name, "key") 68 | 69 | assert isinstance(response, CacheGet.Miss) 70 | 71 | retry_count = metrics_collector.get_total_retry_count(cache_name, MomentoRpcMethod.GET) 72 | assert 2 <= retry_count <= 3 73 | -------------------------------------------------------------------------------- /src/momento/internal/synchronous/_scs_token_client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from momento_wire_types import token_pb2 as token_pb 4 | from momento_wire_types import token_pb2_grpc as token_grpc 5 | 6 | from momento import logs 7 | from momento.auth.access_control.disposable_token_scope import DisposableTokenProps, DisposableTokenScope 8 | from momento.auth.credential_provider import CredentialProvider 9 | from momento.config.auth_configuration import AuthConfiguration 10 | from momento.errors.error_converter import convert_error 11 | from momento.internal._utilities._data_validation import _validate_disposable_token_expiry 12 | from momento.internal._utilities._permissions import permissions_from_disposable_token_scope 13 | from momento.internal.services import Service 14 | from momento.internal.synchronous._scs_grpc_manager import _TokenGrpcManager 15 | from momento.responses.auth.generate_disposable_token import GenerateDisposableToken, GenerateDisposableTokenResponse 16 | from momento.utilities import ExpiresIn 17 | 18 | 19 | class _ScsTokenClient: 20 | """Momento Internal token client.""" 21 | 22 | def __init__(self, configuration: AuthConfiguration, credential_provider: CredentialProvider): 23 | endpoint = credential_provider.token_endpoint 24 | self._logger = logs.logger 25 | self._logger.debug("Token client instantiated with endpoint: %s", endpoint) 26 | self._grpc_manager = _TokenGrpcManager(configuration, credential_provider) 27 | self._endpoint = endpoint 28 | 29 | @property 30 | def endpoint(self) -> str: 31 | return self._endpoint 32 | 33 | def generate_disposable_token( 34 | self, 35 | permission_scope: DisposableTokenScope, 36 | expires_in: ExpiresIn, 37 | credentialProvider: CredentialProvider, 38 | disposable_token_props: Optional[DisposableTokenProps] = None, 39 | ) -> GenerateDisposableTokenResponse: 40 | try: 41 | _validate_disposable_token_expiry(expires_in) 42 | self._logger.info("Creating disposable token") 43 | 44 | token_id = disposable_token_props.token_id if disposable_token_props else None 45 | expires = token_pb._GenerateDisposableTokenRequest.Expires(valid_for_seconds=expires_in.valid_for_seconds()) 46 | permissions = permissions_from_disposable_token_scope(permission_scope) 47 | 48 | request = token_pb._GenerateDisposableTokenRequest( 49 | auth_token=credentialProvider.get_auth_token(), 50 | expires=expires, 51 | permissions=permissions, 52 | token_id=token_id, 53 | ) 54 | response = self._build_stub().GenerateDisposableToken(request) # type: ignore[misc] 55 | return GenerateDisposableToken.Success.from_grpc_response(response) # type: ignore[misc] 56 | except Exception as e: 57 | self._logger.debug("Failed to generate disposable token with exception: %s", e) 58 | return GenerateDisposableToken.Error(convert_error(e, Service.AUTH)) 59 | 60 | def _build_stub(self) -> token_grpc.TokenStub: 61 | return self._grpc_manager.stub() 62 | 63 | def close(self) -> None: 64 | self._grpc_manager.close() 65 | -------------------------------------------------------------------------------- /src/momento/internal/aio/_scs_token_client.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from momento_wire_types import token_pb2 as token_pb 4 | from momento_wire_types import token_pb2_grpc as token_grpc 5 | 6 | from momento import logs 7 | from momento.auth.access_control.disposable_token_scope import DisposableTokenProps, DisposableTokenScope 8 | from momento.auth.credential_provider import CredentialProvider 9 | from momento.config.auth_configuration import AuthConfiguration 10 | from momento.errors.error_converter import convert_error 11 | from momento.internal._utilities._data_validation import _validate_disposable_token_expiry 12 | from momento.internal._utilities._permissions import permissions_from_disposable_token_scope 13 | from momento.internal.aio._scs_grpc_manager import _TokenGrpcManager 14 | from momento.internal.services import Service 15 | from momento.responses.auth.generate_disposable_token import GenerateDisposableToken, GenerateDisposableTokenResponse 16 | from momento.utilities import ExpiresIn 17 | 18 | 19 | class _ScsTokenClient: 20 | """Momento Internal token client.""" 21 | 22 | def __init__(self, configuration: AuthConfiguration, credential_provider: CredentialProvider): 23 | endpoint = credential_provider.token_endpoint 24 | self._logger = logs.logger 25 | self._logger.debug("Token client instantiated with endpoint: %s", endpoint) 26 | self._grpc_manager = _TokenGrpcManager(configuration, credential_provider) 27 | self._endpoint = endpoint 28 | 29 | @property 30 | def endpoint(self) -> str: 31 | return self._endpoint 32 | 33 | async def generate_disposable_token( 34 | self, 35 | permission_scope: DisposableTokenScope, 36 | expires_in: ExpiresIn, 37 | credentialProvider: CredentialProvider, 38 | disposable_token_props: Optional[DisposableTokenProps] = None, 39 | ) -> GenerateDisposableTokenResponse: 40 | try: 41 | _validate_disposable_token_expiry(expires_in) 42 | self._logger.info("Creating disposable token") 43 | 44 | token_id = disposable_token_props.token_id if disposable_token_props else None 45 | expires = token_pb._GenerateDisposableTokenRequest.Expires(valid_for_seconds=expires_in.valid_for_seconds()) 46 | permissions = permissions_from_disposable_token_scope(permission_scope) 47 | 48 | request = token_pb._GenerateDisposableTokenRequest( 49 | auth_token=credentialProvider.get_auth_token(), 50 | expires=expires, 51 | permissions=permissions, 52 | token_id=token_id, 53 | ) 54 | response = await self._build_stub().GenerateDisposableToken(request) # type: ignore[misc] 55 | return GenerateDisposableToken.Success.from_grpc_response(response) # type: ignore[misc] 56 | except Exception as e: 57 | self._logger.debug("Failed to generate disposable token with exception: %s", e) 58 | return GenerateDisposableToken.Error(convert_error(e, Service.AUTH)) 59 | 60 | def _build_stub(self) -> token_grpc.TokenStub: 61 | return self._grpc_manager.async_stub() 62 | 63 | async def close(self) -> None: 64 | await self._grpc_manager.close() 65 | -------------------------------------------------------------------------------- /tests/momento/local/test_fixed_count_retry_strategy_async.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from momento.errors import MomentoErrorCode 3 | from momento.responses import CacheGet, CacheIncrement 4 | 5 | from tests.conftest import client_async_local 6 | from tests.momento.local.momento_local_async_middleware import MomentoLocalMiddlewareArgs 7 | from tests.momento.local.momento_local_metrics_collector import MomentoLocalMetricsCollector 8 | from tests.momento.local.momento_rpc_method import MomentoRpcMethod 9 | from tests.utils import uuid_str 10 | 11 | 12 | @pytest.mark.asyncio 13 | @pytest.mark.local 14 | async def test_retry_eligible_api_should_make_max_attempts_when_full_network_outage() -> None: 15 | metrics_collector = MomentoLocalMetricsCollector() 16 | middleware_args = MomentoLocalMiddlewareArgs( 17 | request_id=str(uuid_str()), 18 | test_metrics_collector=metrics_collector, 19 | return_error=MomentoErrorCode.SERVER_UNAVAILABLE, 20 | error_rpc_list=[MomentoRpcMethod.GET], 21 | ) 22 | cache_name = uuid_str() 23 | 24 | async with client_async_local(cache_name, middleware_args) as client: 25 | response = await client.get(cache_name, "key") 26 | 27 | assert isinstance(response, CacheGet.Error) 28 | assert response.error_code == MomentoErrorCode.SERVER_UNAVAILABLE 29 | 30 | retry_count = metrics_collector.get_total_retry_count(cache_name, MomentoRpcMethod.GET) 31 | assert retry_count == 3 32 | 33 | 34 | @pytest.mark.asyncio 35 | @pytest.mark.local 36 | async def test_non_retry_eligible_api_should_make_no_attempts_when_full_network_outage() -> None: 37 | metrics_collector = MomentoLocalMetricsCollector() 38 | middleware_args = MomentoLocalMiddlewareArgs( 39 | request_id=str(uuid_str()), 40 | test_metrics_collector=metrics_collector, 41 | return_error=MomentoErrorCode.SERVER_UNAVAILABLE, 42 | error_rpc_list=[MomentoRpcMethod.INCREMENT], 43 | ) 44 | cache_name = uuid_str() 45 | 46 | async with client_async_local(cache_name, middleware_args) as client: 47 | response = await client.increment(cache_name, "key", 1) 48 | 49 | assert isinstance(response, CacheIncrement.Error) 50 | assert response.error_code == MomentoErrorCode.SERVER_UNAVAILABLE 51 | 52 | retry_count = metrics_collector.get_total_retry_count(cache_name, MomentoRpcMethod.INCREMENT) 53 | assert retry_count == 0 54 | 55 | 56 | @pytest.mark.asyncio 57 | @pytest.mark.local 58 | async def test_retry_eligible_api_should_make_less_than_max_attempts_when_temporary_network_outage() -> None: 59 | metrics_collector = MomentoLocalMetricsCollector() 60 | middleware_args = MomentoLocalMiddlewareArgs( 61 | request_id=str(uuid_str()), 62 | test_metrics_collector=metrics_collector, 63 | return_error=MomentoErrorCode.SERVER_UNAVAILABLE, 64 | error_rpc_list=[MomentoRpcMethod.GET], 65 | error_count=2, 66 | ) 67 | cache_name = uuid_str() 68 | 69 | async with client_async_local(cache_name, middleware_args) as client: 70 | response = await client.get(cache_name, "key") 71 | 72 | assert isinstance(response, CacheGet.Miss) 73 | 74 | retry_count = metrics_collector.get_total_retry_count(cache_name, MomentoRpcMethod.GET) 75 | assert 2 <= retry_count <= 3 76 | -------------------------------------------------------------------------------- /tests/momento/cache_client/test_init.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | import pytest 5 | from momento import CacheClient 6 | from momento.auth import CredentialProvider 7 | from momento.config import Configuration 8 | from momento.errors import InvalidArgumentException 9 | from momento.errors.exceptions import ConnectionException 10 | 11 | 12 | def test_init_throws_exception_when_client_uses_negative_default_ttl( 13 | configuration: Configuration, credential_provider: CredentialProvider 14 | ) -> None: 15 | with pytest.raises(InvalidArgumentException, match="TTL must be a positive amount of time."): 16 | CacheClient(configuration, credential_provider, timedelta(seconds=-1)) 17 | 18 | 19 | def test_init_throws_exception_for_non_jwt_token(configuration: Configuration, default_ttl_seconds: timedelta) -> None: 20 | with pytest.raises(InvalidArgumentException, match="Invalid Auth token."): 21 | os.environ["BAD_API_KEY"] = "notanauthtoken" 22 | credential_provider = CredentialProvider.from_environment_variable("BAD_API_KEY") 23 | CacheClient(configuration, credential_provider, default_ttl_seconds) 24 | 25 | 26 | def test_init_throws_exception_when_client_uses_integer_request_timeout_ms( 27 | configuration: Configuration, credential_provider: CredentialProvider, default_ttl_seconds: int 28 | ) -> None: 29 | with pytest.raises(InvalidArgumentException, match="Request timeout must be a timedelta."): 30 | configuration.with_client_timeout(-1) # type: ignore[arg-type] 31 | 32 | 33 | def test_init_throws_exception_when_client_uses_negative_request_timeout_ms( 34 | configuration: Configuration, credential_provider: CredentialProvider, default_ttl_seconds: timedelta 35 | ) -> None: 36 | with pytest.raises(InvalidArgumentException, match="Request timeout must be a positive amount of time."): 37 | configuration = configuration.with_client_timeout(timedelta(seconds=-1)) 38 | CacheClient(configuration, credential_provider, default_ttl_seconds) 39 | 40 | 41 | def test_init_throws_exception_when_client_uses_zero_request_timeout_ms( 42 | configuration: Configuration, credential_provider: CredentialProvider, default_ttl_seconds: timedelta 43 | ) -> None: 44 | with pytest.raises(InvalidArgumentException, match="Request timeout must be a positive amount of time."): 45 | configuration = configuration.with_client_timeout(timedelta(seconds=0)) 46 | CacheClient(configuration, credential_provider, default_ttl_seconds) 47 | 48 | 49 | def test_init_eagerly_connecting_cache_client( 50 | configuration: Configuration, credential_provider: CredentialProvider, default_ttl_seconds: timedelta 51 | ) -> None: 52 | client = CacheClient.create( 53 | configuration, credential_provider, default_ttl_seconds, eager_connection_timeout=timedelta(seconds=30) 54 | ) 55 | assert client 56 | 57 | 58 | def test_init_cache_client_eager_connection_failure( 59 | configuration: Configuration, credential_provider: CredentialProvider, default_ttl_seconds: timedelta 60 | ) -> None: 61 | with pytest.raises( 62 | ConnectionException, match="Failed to connect to Momento's server within given eager connection timeout" 63 | ): 64 | CacheClient.create( 65 | configuration, credential_provider, default_ttl_seconds, eager_connection_timeout=timedelta(milliseconds=1) 66 | ) 67 | -------------------------------------------------------------------------------- /src/momento/responses/auth/generate_disposable_token.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import json 5 | from abc import ABC 6 | from dataclasses import dataclass 7 | 8 | from momento_wire_types import token_pb2 as token_pb 9 | 10 | from momento.responses.response import AuthResponse 11 | from momento.utilities import ExpiresAt 12 | 13 | from ..mixins import ErrorResponseMixin 14 | 15 | 16 | class GenerateDisposableTokenResponse(AuthResponse): 17 | """Parent response type for a generate disposable token request. 18 | 19 | The response object is resolved to a type-safe object of one of 20 | the following subtypes: 21 | - `GenerateDisposableToken.Success` 22 | - `GenerateDisposableToken.Error` 23 | 24 | Pattern matching can be used to operate on the appropriate subtype. 25 | For example, in python 3.10+:: 26 | 27 | match response: 28 | case GenerateDisposableToken.Success(): 29 | ... 30 | case GenerateDisposableToken.Error(): 31 | ... 32 | case _: 33 | # Shouldn't happen 34 | 35 | or equivalently in earlier versions of python:: 36 | 37 | if isinstance(response, GenerateDisposableToken.Success): 38 | ... 39 | elif isinstance(response, GenerateDisposableToken.Error): 40 | ... 41 | else: 42 | # Shouldn't happen 43 | """ 44 | 45 | 46 | class GenerateDisposableToken(ABC): 47 | """Groups all `GenerateDisposableTokenResponse` derived types under a common namespace.""" 48 | 49 | @dataclass 50 | class Success(GenerateDisposableTokenResponse): 51 | """Indicates the request was successful.""" 52 | 53 | auth_token: str 54 | """The generated disposable token.""" 55 | 56 | endpoint: str 57 | """The endpoint the Momento client should use when making requests.""" 58 | 59 | expires_at: ExpiresAt 60 | """The time at which the disposable token will expire.""" 61 | 62 | def __init__(self, auth_token: str, endpoint: str, expires_at: ExpiresAt): 63 | self.auth_token = auth_token 64 | self.endpoint = endpoint 65 | self.expires_at = expires_at 66 | 67 | @staticmethod 68 | def from_grpc_response( 69 | grpc_response: token_pb._GenerateDisposableTokenResponse, 70 | ) -> GenerateDisposableToken.Success: 71 | """Initializes GenerateDisposableTokenResponse to handle generate disposable token response. 72 | 73 | Args: 74 | grpc_response: Protobuf based response returned by token service. 75 | """ 76 | to_b64_encode = {"endpoint": grpc_response.endpoint, "api_key": grpc_response.api_key} 77 | byte_string = json.dumps(to_b64_encode).encode("utf-8") 78 | auth_token = base64.b64encode(byte_string).decode("utf-8") 79 | return GenerateDisposableToken.Success( 80 | auth_token, grpc_response.endpoint, ExpiresAt(grpc_response.valid_until) 81 | ) 82 | 83 | class Error(GenerateDisposableTokenResponse, ErrorResponseMixin): 84 | """Contains information about an error returned from a request. 85 | 86 | This includes: 87 | - `error_code`: `MomentoErrorCode` value for the error. 88 | - `messsage`: a detailed error message. 89 | """ 90 | -------------------------------------------------------------------------------- /src/momento/auth/access_control/permission_scope.py: -------------------------------------------------------------------------------- 1 | """Permission Scope. 2 | 3 | Defines the data classes for specifying the permission scope of a generated token. 4 | """ 5 | from dataclasses import dataclass 6 | from enum import Enum 7 | from typing import List, Union 8 | 9 | 10 | @dataclass 11 | class AllCaches: 12 | """Indicates permission to access all caches.""" 13 | 14 | pass 15 | 16 | 17 | @dataclass 18 | class AllTopics: 19 | """Indicates permission to access all topics.""" 20 | 21 | pass 22 | 23 | 24 | @dataclass 25 | class PredefinedScope: 26 | """Indicates a predefined permission scope.""" 27 | 28 | pass 29 | 30 | 31 | class CacheRole(Enum): 32 | """The permission level for a cache.""" 33 | 34 | READ_WRITE = "read_write" 35 | READ_ONLY = "read_only" 36 | WRITE_ONLY = "write_only" 37 | 38 | 39 | @dataclass 40 | class CacheName: 41 | """The name of a cache.""" 42 | 43 | name: str 44 | 45 | 46 | @dataclass 47 | class CacheSelector: 48 | """A selection of caches to grant permissions to, either all caches or a specific cache.""" 49 | 50 | cache: Union[CacheName, AllCaches, str] 51 | 52 | def is_all_caches(self) -> bool: 53 | """Check if the cache selector is for all caches.""" 54 | return isinstance(self.cache, AllCaches) 55 | 56 | 57 | @dataclass 58 | class CachePermission: 59 | """Encapsulates the information needed to grant permissions to a cache.""" 60 | 61 | cache_selector: CacheSelector 62 | role: CacheRole 63 | 64 | 65 | class TopicRole(Enum): 66 | """The permission level for a topic.""" 67 | 68 | PUBLISH_SUBSCRIBE = "publish_subscribe" 69 | SUBSCRIBE_ONLY = "subscribe_only" 70 | PUBLISH_ONLY = "publish_only" 71 | 72 | 73 | @dataclass 74 | class TopicName: 75 | """The name of a topic.""" 76 | 77 | name: str 78 | 79 | 80 | @dataclass 81 | class TopicSelector: 82 | """A selection of topics to grant permissions to, either all topics or a specific topic.""" 83 | 84 | topic: Union[TopicName, AllTopics, str] 85 | 86 | def is_all_topics(self) -> bool: 87 | return isinstance(self.topic, AllTopics) 88 | 89 | 90 | @dataclass 91 | class TopicPermission: 92 | """Encapsulates the information needed to grant permissions to a topic.""" 93 | 94 | role: TopicRole 95 | cache_selector: CacheSelector 96 | topic_selector: TopicSelector 97 | 98 | 99 | Permission = Union[CachePermission, TopicPermission] 100 | 101 | 102 | @dataclass 103 | class Permissions: 104 | """A list of permissions to grant to an API key.""" 105 | 106 | permissions: List[Permission] 107 | 108 | 109 | ALL_DATA_READ_WRITE = Permissions( 110 | permissions=[ 111 | CachePermission(CacheSelector(AllCaches()), CacheRole.READ_WRITE), 112 | TopicPermission( 113 | TopicRole.PUBLISH_SUBSCRIBE, 114 | CacheSelector(AllCaches()), 115 | TopicSelector(AllTopics()), 116 | ), 117 | ] 118 | ) 119 | 120 | 121 | @dataclass 122 | class PermissionScope: 123 | """A set of permissions to grant to an API key, either a predefined scope or a custom scope.""" 124 | 125 | permission_scope: Union[Permissions, PredefinedScope] 126 | 127 | def is_all_data_read_write(self) -> bool: 128 | return self.permission_scope == ALL_DATA_READ_WRITE 129 | -------------------------------------------------------------------------------- /src/momento/utilities/expiration.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | from dataclasses import dataclass 5 | from datetime import datetime 6 | from typing import Optional 7 | 8 | 9 | @dataclass 10 | class Expiration: 11 | """Represents an expiration time for a token.""" 12 | 13 | def __init__(self, does_expire: bool) -> None: 14 | self._does_expire = does_expire 15 | 16 | def does_expire(self) -> bool: 17 | return self._does_expire 18 | 19 | 20 | @dataclass 21 | class ExpiresIn(Expiration): 22 | """Represents an expiration time for a token that expires in a certain amount of time.""" 23 | 24 | # Must be a float in order to use math.inf to indicate non-expiration 25 | def __init__(self, valid_for: Optional[float] = math.inf) -> None: 26 | super().__init__(valid_for != math.inf) 27 | if valid_for is None: 28 | self._valid_for = math.inf 29 | else: 30 | self._valid_for = valid_for 31 | 32 | def valid_for_seconds(self) -> int: 33 | return int(self._valid_for) 34 | 35 | @staticmethod 36 | def never() -> ExpiresIn: 37 | return ExpiresIn(math.inf) 38 | 39 | @staticmethod 40 | def seconds(valid_for_seconds: int) -> ExpiresIn: 41 | """Constructs a ExpiresIn with a specified valid_for period in seconds. If seconds are undefined, or null, then token never expires.""" 42 | return ExpiresIn(valid_for_seconds) 43 | 44 | @staticmethod 45 | def minutes(valid_for_minutes: int) -> ExpiresIn: 46 | """Constructs a ExpiresIn with a specified valid_for period in minutes.""" 47 | return ExpiresIn(valid_for_minutes * 60) 48 | 49 | @staticmethod 50 | def hours(valid_for_hours: int) -> ExpiresIn: 51 | """Constructs a ExpiresIn with a specified valid_for period in hours.""" 52 | return ExpiresIn(valid_for_hours * 3600) 53 | 54 | @staticmethod 55 | def days(valid_for_days: int) -> ExpiresIn: 56 | """Constructs an ExpiresIn with a specified valid_for period in days.""" 57 | return ExpiresIn(valid_for_days * 86400) 58 | 59 | @staticmethod 60 | def epoch(expires_by: int) -> ExpiresIn: 61 | """Constructs an ExpiresIn with a specified expires_by period in epoch format.""" 62 | current_epoch = int(datetime.now().timestamp()) 63 | seconds_until_epoch = expires_by - current_epoch 64 | return ExpiresIn(seconds_until_epoch) 65 | 66 | 67 | @dataclass 68 | class ExpiresAt(Expiration): 69 | """Represents an expiration time for a token that expires at a certain UNIX epoch timestamp.""" 70 | 71 | # Must be a float in order to use math.inf to indicate non-expiration 72 | def __init__(self, epoch_timestamp: Optional[float] = None) -> None: 73 | super().__init__(epoch_timestamp is not None and epoch_timestamp != 0) 74 | if self._does_expire and epoch_timestamp is not None: 75 | self._expires_at = epoch_timestamp 76 | else: 77 | self._expires_at = math.inf 78 | 79 | def epoch(self) -> int: 80 | """Returns epoch timestamp of when api token expires.""" 81 | return int(self._expires_at) 82 | 83 | @staticmethod 84 | def from_epoch(epoch: Optional[int] = None) -> ExpiresAt: 85 | """Constructs an ExpiresAt with the specified epoch timestamp. If timestamp is undefined, then epoch timestamp will instead be Infinity.""" 86 | return ExpiresAt(epoch) 87 | --------------------------------------------------------------------------------