Source code for are.simulation.apps.city

# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.


import logging
from dataclasses import dataclass, field
from typing import Any

from are.simulation.apps.app import App
from are.simulation.tool_utils import OperationType, app_tool, env_tool
from are.simulation.types import EventType, event_registered
from are.simulation.utils import get_state_dict, type_check


[docs] @dataclass class CrimeDataPoint: violent_crime: float property_crime: float
logger = logging.getLogger(__name__)
[docs] @dataclass class CityApp(App): """ A city information application that provides access to crime rate data for different zip codes. This class implements a rate-limited API service for retrieving crime statistics, with built-in tracking of API usage and enforcement of call limits. The CityApp maintains crime data in a dictionary where each zip code is mapped to its corresponding crime statistics. The free version of the API has usage limitations that are strictly enforced. Key Features: - Crime Data Access: Retrieve crime statistics for specific zip codes - Rate Limiting: Implements usage restrictions with 30-minute cooldown - API Usage Tracking: Monitors and manages API call counts - State Management: Save and load application state - Data Loading: Support for loading crime data from files or dictionaries Notes: - Free version is limited to 100 API calls per 30-minute period - Rate limit reset requires a 30-minute waiting period - API calls are tracked and persist until manually reset - Attempting to exceed rate limits raises an exception - Invalid zip codes result in ValueError """ name: str | None = None api_call_count: int = 0 api_call_limit: int = 100 rate_limit_cooldown_seconds = 1800 # 30 minutes crime_data: dict[str, CrimeDataPoint] = field(default_factory=dict) rate_limit_time: float | None = None rate_limit_exceeded: bool = False def __post_init__(self): super().__init__(self.name)
[docs] def get_state(self) -> dict[str, Any] | None: return get_state_dict(self, ["api_call_limit", "crime_data"])
[docs] def load_state(self, state_dict: dict[str, Any]): self.load_crime_data_from_dict(state_dict["crime_data"]) self.api_call_limit = state_dict["api_call_limit"]
[docs] def reset(self): super().reset() self.crime_data = {} self.api_call_count = 0
[docs] def load_crime_data_from_dict(self, crime_data): self.crime_data = {} for zipcode in crime_data: self.crime_data[zipcode] = CrimeDataPoint(**crime_data[zipcode])
def _is_rate_limited(self) -> bool: """Check if API calls are currently rate limited.""" # First time hitting the limit if self.api_call_count >= self.api_call_limit and self.rate_limit_time is None: return True # Still in cooldown period if self.rate_limit_time is not None: time_since_limit = self.time_manager.time() - self.rate_limit_time return time_since_limit < self.rate_limit_cooldown_seconds return False def _enforce_rate_limit(self) -> None: """Enforce rate limiting by setting state and raising exception.""" if self.rate_limit_time is None: self.rate_limit_time = self.time_manager.time() self.reset_api_call_count() self.rate_limit_exceeded = True raise ValueError( f"Free version only supports {self.api_call_limit} API calls. " f"Please try again after 30 minutes or upgrade to pro-version." ) def _reset_rate_limit_if_expired(self) -> None: """Reset rate limit if cooldown period has passed.""" if ( self.rate_limit_time is not None and self.time_manager.time() - self.rate_limit_time >= self.rate_limit_cooldown_seconds ): self.rate_limit_time = None self.rate_limit_exceeded = False
[docs] @env_tool() @type_check @event_registered(operation_type=OperationType.WRITE, event_type=EventType.ENV) def add_crime_rate( self, zip_code: str, violent_crime_rate: float, property_crime_rate: float ) -> str: """ Add crime rate for a given zip code. :param zip_code: zip code to add crime rate for :param violent_crime_rate: violent crime rate :param property_crime_rate: property crime rate :return: Success message """ self.crime_data[zip_code] = CrimeDataPoint( violent_crime=violent_crime_rate, property_crime=property_crime_rate, ) return "Added Successfully"
[docs] @env_tool() @type_check @event_registered(operation_type=OperationType.WRITE, event_type=EventType.ENV) def update_crime_rate( self, zip_code: str, new_violent_crime_rate: float | None = None, new_property_crime_rate: float | None = None, ) -> str: """ Update crime rate for a given zip code. :param zip_code: zip code to update crime rate for :param new_violent_crime_rate: violent crime rate :param new_property_crime_rate: property crime rate :return: Success message """ if new_violent_crime_rate is None and new_property_crime_rate is None: raise ValueError("No update provided") if zip_code not in self.crime_data: raise ValueError("Zip code does not exist in our database") crime_datapoint = self.crime_data[zip_code] if new_violent_crime_rate is not None: crime_datapoint.violent_crime = new_violent_crime_rate if new_property_crime_rate is not None: crime_datapoint.property_crime = new_property_crime_rate return "Updated Successfully"
[docs] @type_check @app_tool() @event_registered(operation_type=OperationType.READ) def get_crime_rate(self, zip_code: str) -> CrimeDataPoint: """ Get crime rate for a given zip code. This is a free version of the API, so it has a limit. This limit can be obtained by calling get_api_call_limit() method. If you exceed this limit, you have to wait for 30 minutes to make more calls. :param zip_code: zip code to get crime rate for :returns: crime rate details """ # Reset rate limit if cooldown expired self._reset_rate_limit_if_expired() # Check and enforce rate limiting if self._is_rate_limited(): self._enforce_rate_limit() self.api_call_count += 1 if zip_code not in self.crime_data: raise ValueError("Zip code does not exist in our database") return self.crime_data[zip_code]
[docs] def reset_api_call_count(self) -> None: """ Reset the API call count :returns: None """ self.api_call_count = 0
[docs] @app_tool() @event_registered(operation_type=OperationType.READ) def get_api_call_count(self) -> int: """ Get the current API call count for the service. :returns: API call count """ return self.api_call_count
[docs] @app_tool() @event_registered(operation_type=OperationType.READ) def get_api_call_limit(self) -> int: """ Get the API call limit rate for the service. :returns: API call count """ return self.api_call_limit