# 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.importloggingfromdataclassesimportdataclass,fieldfromtypingimportAnyfromare.simulation.apps.appimportAppfromare.simulation.tool_utilsimportOperationType,app_tool,env_toolfromare.simulation.typesimportEventType,event_registeredfromare.simulation.utilsimportget_state_dict,type_check
[docs]@dataclassclassCityApp(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=Noneapi_call_count:int=0api_call_limit:int=100rate_limit_cooldown_seconds=1800# 30 minutescrime_data:dict[str,CrimeDataPoint]=field(default_factory=dict)rate_limit_time:float|None=Nonerate_limit_exceeded:bool=Falsedef__post_init__(self):super().__init__(self.name)
def_is_rate_limited(self)->bool:"""Check if API calls are currently rate limited."""# First time hitting the limitifself.api_call_count>=self.api_call_limitandself.rate_limit_timeisNone:returnTrue# Still in cooldown periodifself.rate_limit_timeisnotNone:time_since_limit=self.time_manager.time()-self.rate_limit_timereturntime_since_limit<self.rate_limit_cooldown_secondsreturnFalsedef_enforce_rate_limit(self)->None:"""Enforce rate limiting by setting state and raising exception."""ifself.rate_limit_timeisNone:self.rate_limit_time=self.time_manager.time()self.reset_api_call_count()self.rate_limit_exceeded=TrueraiseValueError(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_timeisnotNoneandself.time_manager.time()-self.rate_limit_time>=self.rate_limit_cooldown_seconds):self.rate_limit_time=Noneself.rate_limit_exceeded=False
[docs]@env_tool()@type_check@event_registered(operation_type=OperationType.WRITE,event_type=EventType.ENV)defadd_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)defupdate_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 """ifnew_violent_crime_rateisNoneandnew_property_crime_rateisNone:raiseValueError("No update provided")ifzip_codenotinself.crime_data:raiseValueError("Zip code does not exist in our database")crime_datapoint=self.crime_data[zip_code]ifnew_violent_crime_rateisnotNone:crime_datapoint.violent_crime=new_violent_crime_rateifnew_property_crime_rateisnotNone:crime_datapoint.property_crime=new_property_crime_ratereturn"Updated Successfully"
[docs]@type_check@app_tool()@event_registered(operation_type=OperationType.READ)defget_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 expiredself._reset_rate_limit_if_expired()# Check and enforce rate limitingifself._is_rate_limited():self._enforce_rate_limit()self.api_call_count+=1ifzip_codenotinself.crime_data:raiseValueError("Zip code does not exist in our database")returnself.crime_data[zip_code]
[docs]defreset_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)defget_api_call_count(self)->int:""" Get the current API call count for the service. :returns: API call count """returnself.api_call_count
[docs]@app_tool()@event_registered(operation_type=OperationType.READ)defget_api_call_limit(self)->int:""" Get the API call limit rate for the service. :returns: API call count """returnself.api_call_limit