def test_number_serialize(self): """ NumberAttribute.serialize """ attr = NumberAttribute() self.assertEqual(attr.serialize(3.141), '3.141') self.assertEqual(attr.serialize(1), '1')
def test_number_deserialize(self): """ NumberAttribute.deserialize """ attr = NumberAttribute() self.assertEqual(attr.deserialize('1'), 1) self.assertEqual(attr.deserialize('3.141'), 3.141)
def test_number_deserialize(self): """ NumberAttribute.deserialize """ attr = NumberAttribute() assert attr.deserialize('1') == 1 assert attr.deserialize('3.141') == 3.141 assert attr.deserialize('12345678909876543211234234324234') == 12345678909876543211234234324234
def test_number_serialize(self): """ NumberAttribute.serialize """ attr = NumberAttribute() assert attr.serialize(3.141) == '3.141' assert attr.serialize(1) == '1' assert attr.serialize(12345678909876543211234234324234) == '12345678909876543211234234324234'
class MeasuredDuration(Model): class Meta: table_name = 'aws-scheduler-testing' id = UnicodeAttribute(hash_key=True) delay = NumberAttribute()
class Product(Model): class Meta: table_name = "product" region = 'ap-northeast-2' id = UnicodeAttribute(hash_key=True) do_crawl = BooleanAttribute(default=False) created_at = UnicodeAttribute() last_crawled_at = UTCDateTimeAttribute(null=True) min_price = NumberAttribute(default=0) # min_price 가격 이상인 결과만 사용 queries = ListAttribute(of=ShoppingQuery) lprice = NumberAttribute(null=True) # 현재까지 최저 가격 lprice_item = MapAttribute(null=True) # 최저가격의 item 정보 def update_lprice(self, lprice_item): self.last_crawled_at = datetime.datetime.utcnow() if lprice_item: self.lprice_item = lprice_item self.lprice = int(lprice_item['lprice']) record = PriceRecord(self.id, self.last_crawled_at) record.item = lprice_item record.save() return self.save() def update_last_crawled_at(self): self.last_crawled_at = datetime.datetime.utcnow() return self.save() def search_lowest_price(self): # 최저가를 찾고, 최저가 발견시 알림 min_price_criterion = self.min_price lprice = self.lprice or 100000000 lprice_item = None try: for q in self.queries: api = naver_shopping_api.format(quote(q.query), q.display or DEFAULT_DISPLAY, q.sort or DEFAULT_SORT) api_response = requests.get(api, headers=naver_api_header) if api_response.status_code == 200: response_json = api_response.json() if not response_json.get('total', 0): # TODO: no item warning noti pass for item in response_json.get('items', {}): item_lprice = int(item['lprice']) if min_price_criterion <= item_lprice < lprice: lprice = item_lprice lprice_item = item lprice_item['query'] = q.attribute_values else: error_message = { 'message': 'Naver API Error for {}'.format(self.id), 'status_code': api_response.status_code, 'text': api_response.text, 'api': api, 'query': q.attribute_values } send_slack_notification( build_naver_warning_slack_message(error_message)) except Exception as e: error_message = { 'message': 'Exception occurs by {}'.format(self.id), 'exception': e, 'traceback': traceback.format_exc() } send_slack_notification(build_error_slack_message(error_message)) self.update_lprice(lprice_item) if lprice_item: # print(build_normal_slack_message(self)) status_code = send_slack_notification( build_normal_slack_message(self)) return lprice_item
class CustomAttrMap(MapAttribute): overridden_number_attr = NumberAttribute(attr_name="number_attr") overridden_unicode_attr = UnicodeAttribute(attr_name="unicode_attr")
class SubModel(Model): key = NumberAttribute(hash_key=True) sub_map = SubMapAttribute(attr_name='dyn_sub_map')
class MyModel(Model): class Meta: table_name = 'some-table' notmyid = UnicodeAttribute(null=True) myid = NumberAttribute(hash_key=True)
class DifferentRegion(Model): class Meta: region = 'us-east-2' table_name = 'different-region' entry_index = NumberAttribute(hash_key=True)
class SomeModel(Model): key = NumberAttribute(hash_key=True) typed_map = TypedMap()
def test_should_number_convert_int(): assert_attribute_conversion(NumberAttribute(), graphene.Int)
class User(Model): class Meta: region = 'us-east-1' table_name = 'user' user_id = NumberAttribute(hash_key=True)
class Position(MapAttribute): lat = NumberAttribute(null=False) lng = NumberAttribute(null=False)
class PetLocation(MapAttribute): position = Position(null=False) at = NumberAttribute(null=False, default=lambda: datetime.timestamp(datetime.now()))
class Zone(MapAttribute): position = Position(null=False) radius = NumberAttribute(null=False)
class FoodAttribute(MapAttribute): food_name = UnicodeAttribute(null=False) food_calorie = NumberAttribute(null=False)
class CoordinateMap(MapAttribute): lat = NumberAttribute() lon = NumberAttribute()
class MotionAttribute(MapAttribute): motion_name = UnicodeAttribute(null=False) motion_calorie = NumberAttribute(null=False)
class ResourceUpdatedAtIndex(LocalSecondaryIndex): class Meta: projection = AllProjection() site = UnicodeAttribute(hash_key=True) updated_at = NumberAttribute(range_key=True)
class ThingModel(Model): key = NumberAttribute(hash_key=True) nested = NestedThing()
class Location(MapAttribute): lat = NumberAttribute(null=False) lon = NumberAttribute(null=False)
class MyModel(Model): key = NumberAttribute(hash_key=True) outer_map = OuterMapAttribute(attr_name='dyn_out_map')
class Note(MapAttribute): content = UnicodeAttribute(null=True) created_at = NumberAttribute(attr_name="createdAt", default=int(datetime.now().strftime("%s"))) created_by = UnicodeAttribute(attr_name="createdBy")
class ShoppingQuery(MapAttribute): # ref: https://developers.naver.com/docs/search/shopping/ query = UnicodeAttribute() display = NumberAttribute(default=50) sort = UnicodeAttribute(default='sim')
class TwitchVod(Model): """A Twitch VOD link associated with a replay and mapped to a DynamoDB table""" twitch_channel_name = UnicodeAttribute( hash_key=True) # The name of the Twitch channel friendly_player_name = UnicodeAttribute( ) # The BattleTag for the friendly player hsreplaynet_user_id = NumberAttribute( ) # The HSReplay.net user id of the uploader replay_shortid = UnicodeAttribute() # The replay's shortid rank = NumberAttribute() # The friendly player's rank # The friendly player's legend rank, when normal rank is 0, NULL otherwise legend_rank = NumberAttribute(null=True) # A synthetic key composing legend and normal rank to use as a DynamoDB range key combined_rank = UnicodeAttribute(range_key=True) won = BooleanAttribute( ) # True if the friendly player won the game, else False went_first = BooleanAttribute( ) # True if the friendly player went first, else False game_date = NumberAttribute( ) # The epoch second timestamp of the match start game_length_seconds = NumberAttribute( ) # The duration of the game in seconds format_type = UnicodeAttribute( ) # The format of the game (as a FormatType enum name) game_type = UnicodeAttribute( ) # The game type (as a BnetGameType enum name) # The full, canonicalized deck string for the friendly player friendly_player_canonical_deck_string = UnicodeAttribute() # The archetype id for the friendly player's deck, if available, else NULL friendly_player_archetype_id = NumberAttribute(null=True) # The player class of the opposing player (as a CardClass enum name) opposing_player_class = UnicodeAttribute() # The archetype id for the opposing player's deck, if available, else NULL opposing_player_archetype_id = NumberAttribute(null=True) url = UnicodeAttribute() # The URL of the VOD on Twitch archetype_index = TwitchVodArchetypeIndex() deck_index = TwitchVodDeckStringIndex() user_id_index = TwitchVodUserIdIndex() # The TTL, in epoch seconds. DynamoDB must be told to use this attribute as the row TTL; # see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html ttl = NumberAttribute() class Meta: table_name = settings.DYNAMODB_TABLES["twitch_vod"]["NAME"] host = settings.DYNAMODB_TABLES["twitch_vod"]["HOST"] if hasattr(settings, "AWS_CREDENTIALS"): aws_access_key_id = settings.AWS_CREDENTIALS["AWS_ACCESS_KEY_ID"] aws_secret_access_key = settings.AWS_CREDENTIALS[ "AWS_SECRET_ACCESS_KEY"] read_capacity_units = 2 write_capacity_units = 5 ttl_days = 14 def __eq__(self, other): """Overrides the default implementation""" if isinstance(other, TwitchVod): return self.__dict__ == other.__dict__ return NotImplemented def __init__(self, *args, ttl=None, **kwargs): effective_ttl = ttl \ if ttl else int(time.time() + TwitchVod.Meta.ttl_days * 24 * 60 * 60) super().__init__(*args, ttl=effective_ttl, **kwargs)
class Beverage(Model): class Meta: table_name = 'Cellar' region = Config.AWS_REGION if local: # Use the local DynamoDB instance when running locally host = 'http://localhost:8008' # Primary Attributes # `beverage_id`: Concat of producer, beverage name, year, size, and {batch or bottle date}. beverage_id = UnicodeAttribute(hash_key=True) # Required Attributes producer = UnicodeAttribute() name = UnicodeAttribute() year = NumberAttribute() size = UnicodeAttribute() location = UnicodeAttribute(range_key=True) batch = NumberAttribute(null=True) bottle_date = UnicodeAttribute(null=True) # Optional Attributes qty = NumberAttribute(null=True, default=0) qty_cold = NumberAttribute(null=True, default=0) style = UnicodeAttribute(null=True) specific_style = UnicodeAttribute(null=True) for_trade = BooleanAttribute(null=True, default=True) trade_value = NumberAttribute(null=True, default=0) aging_potential = NumberAttribute(null=True, default=2) untappd = UnicodeAttribute(null=True) note = UnicodeAttribute(null=True) # date_added should always be <= last_modified date_added = UTCDateTimeAttribute(default=datetime.utcnow()) last_modified = UTCDateTimeAttribute(default=datetime.utcnow()) def to_dict(self, dates_as_epoch=True) -> dict: """ Return a dictionary with all attributes. Dates return as epoch (default) or in ISO format. """ output = { "beverage_id": self.beverage_id.__str__(), "name": self.name.__str__(), "producer": self.producer.__str__(), "year": int(self.year), "batch": int(self.batch) if self.batch else None, "size": self.size.__str__(), "bottle_date": self.bottle_date.__str__() if self.bottle_date else None, "location": self.location.__str__(), "style": self.style.__str__() if self.style else None, "specific_style": self.specific_style.__str__() if self.specific_style else None, "qty": int(self.qty) if self.qty else 0, "qty_cold": int(self.qty_cold) if self.qty_cold else 0, "untappd": self.untappd.__str__() if self.untappd else None, "aging_potential": int(self.aging_potential) if self.aging_potential else None, "trade_value": int(self.trade_value) if self.trade_value else None, "for_trade": self.for_trade, "note": self.note.__str__() if self.note else None, "date_added": self.date_added.timestamp() * 1000, # JS timestamps are in ms "last_modified": self.last_modified.timestamp() * 1000 } if not dates_as_epoch: output['date_added'] = self.date_added.__str__() output['last_modified'] = self.last_modified.__str__() return output def __init__(self, **kwargs): super().__init__(**kwargs) # logger.debug(f"Initializing a new instance of the Beverage model for {kwargs}.") # Replace empty strings with None # Construct the concatenated beverage_id when not provided: # producer, beverage name, year, size, {bottle date or batch}. Bottle date preferred. if 'beverage_id' not in kwargs.keys(): # Need to create a beverage_id for this beverage self.beverage_id = f"{kwargs['producer']}_{kwargs['name']}_{kwargs['year']}_{kwargs['size']}" if 'batch' in kwargs.keys() and 'bottle_date' in kwargs.keys(): # If both bottle_date and batch are provided, prefer bottle_date self.beverage_id += f"_{kwargs['bottle_date']}" elif 'batch' not in kwargs.keys() or kwargs['batch'] == '': # Batch is not provided self.batch = None if 'bottle_date' not in kwargs.keys( ) or kwargs['bottle_date'] == '': # When no batch or bottle_date is provided, append "_None" self.beverage_id += "_None" self.bottle_date = None else: # Bottle_date is provided self.beverage_id += f"_{kwargs['bottle_date']}" else: # Use batch when bottle_date isn't provided self.beverage_id += f"_{kwargs['batch']}" logger.debug( f"Created a beverage_id for this new Beverage: {self.beverage_id}." ) # Must provide a location if 'location' not in kwargs.keys() or kwargs['location'] is None: logger.debug(f"No value for location provided, raising KeyError.") raise KeyError("Location is required.") # Type check: Year try: self.year = int(kwargs['year']) except ValueError as e: logger.debug(f"Year must be an integer.\n{e}") raise ValueError(f"Year must be an integer.\n{e}") # Type check: Batch if 'batch' in kwargs.keys(): try: if self.batch and self.batch != "": self.batch = int(kwargs['batch']) if self.batch == "": self.batch = None except ValueError as e: logger.debug(f"Batch number must be an integer.\n{e}") raise ValueError(f"Batch number must be an integer.\n{e}") # Type check: qty if 'qty' in kwargs.keys(): try: self.qty = int(kwargs['qty']) except ValueError as e: logger.debug(f"Qty must be an integer.\n{e}") raise ValueError(f"Qty must be an integer.\n{e}") # Type check: qty_cold if 'qty_cold' in kwargs.keys(): try: self.qty_cold = int(kwargs['qty_cold']) except ValueError as e: logger.debug(f"Qty_cold must be an integer.\n{e}") raise ValueError(f"Qty_cold must be an integer.\n{e}") # Adjust last_modified due to JS working in milliseconds if 'last_modified' in kwargs.keys(): # Accept an epoch (`float` or `int`) for date_added if isinstance(self.last_modified, (float, int)): self.last_modified = datetime.utcfromtimestamp( kwargs['last_modified'] / 1000) else: # Assume a string was provided and parse a datetime object from that try: self.last_modified = datetime.fromisoformat( str(self.last_modified)) except (TypeError, ValueError) as e: logger.debug( f"Provided value for last_modified: {self.last_modified}, " f"{type(self.last_modified)} cannot be converted to datetime." ) raise ValueError( f"Value for last_modified must be an epoch (float/int) or " f"an ISO-formatted string. {e}") else: self.last_modified = datetime.utcnow() # Type & value checks for date_added if 'date_added' in kwargs.keys(): # Accept an epoch (`float` or `int`) for date_added if isinstance(self.date_added, (float, int)): self.date_added = datetime.utcfromtimestamp( kwargs['date_added'] / 1000) else: # Assume a string was provided and parse a datetime object from that try: self.date_added = datetime.fromisoformat( str(self.date_added)) except (TypeError, ValueError) as e: logger.debug( f"Provided value for date_added: {self.date_added}, " f"{type(self.date_added)} cannot be converted to datetime." ) raise ValueError( f"Value for date_added must be an epoch (float/int) or " f"an iso-formatted string. {e}") # Ensure date_added is always <= last_modified if self.date_added > self.last_modified: self.last_modified = self.date_added # else: # When date_added is not provided # self.date_added = self.last_modified or datetime.utcnow() def __repr__(self) -> str: return f'<Beverage | beverage_id: {self.beverage_id}, qty: {self.qty} ({self.qty_cold}),' \ f' location: {self.location}>'
class InboundResponse(MapAttribute): body = UnicodeAttribute() headers = ListAttribute(of=HeaderAttribute) status = NumberAttribute()
class CustomMapAttribute(MapAttribute): custom = NumberAttribute() def __eq__(self, other): return self.custom == other.custom
class OAuthToken(Model): """ Stores token data from some OAuth provider """ ALIVE = ALIVE DEAD = DEAD access_token = UnicodeAttribute(hash_key=True) refresh_token = UnicodeAttribute(null=True) token_type = UnicodeAttribute(null=True) # Consistency would be too easy expires_in = NumberAttribute(null=True) expires_at = NumberAttribute(null=True) scope = UnicodeAttribute(null=True) user_info = JSONAttribute(null=True) state_index = TokensByState() state = UnicodeAttribute(default=ALIVE) updated_at = UTCDateTimeAttribute() created_at = UTCDateTimeAttribute(range_key=True) ttl = TTLAttribute(null=True) def save(self, *args, **kwargs): timestamp = timezone.now() if not self.created_at: self.created_at = timestamp self.updated_at = timestamp super().save(*args, **kwargs) def update(self, *args, **kwargs): self.updated_at = timezone.now() super().save(*args, **kwargs) def set_updated_at(self, *args, **kwargs): self.updated_at = timezone.now() def set_expiration(self): self.ttl = datetime.timedelta(days=30) @property def session_data(self): return dict( access_token=self.access_token, token_type=self.token_type, refresh_token=self.refresh_token, expires_at=self.expires_at, expires_in=self.expires_in, ) @classmethod def create_if_non_existent(cls): if not cls.exists(): cls.create_table(read_capacity_units=1, write_capacity_units=1, wait=True) class Meta: table_name = settings.OAUTH_TOKEN_TABLE_NAME region = settings.AWS_REGION
class OccurrenceDetail(MapAttribute): occurrence_num = NumberAttribute() start_date = DateAttribute() end_date = DateAttribute()
class ViewIndex(GlobalSecondaryIndex): class Meta: projection = AllProjection() region = CURRENT_REGION view = NumberAttribute(default=0, hash_key=True)
class CurrentHistoricalModel(object): eventTime = EventTimeAttribute(default=default_event_time) ttl = NumberAttribute(default=default_ttl()) eventSource = UnicodeAttribute()
class HistoricalDecimalAttribute(Attribute): """ A number attribute """ attr_type = NUMBER def serialize(self, value): """ Encode numbers as JSON """ return json.dumps(value, default=decimal_default) def deserialize(self, value): """ Decode numbers from JSON """ return json.loads(value) pynamodb.attributes.SERIALIZE_CLASS_MAP = { dict: MapAttribute(), list: ListAttribute(), set: ListAttribute(), bool: BooleanAttribute(), float: NumberAttribute(), int: NumberAttribute(), str: HistoricalUnicodeAttribute(), decimal.Decimal: HistoricalDecimalAttribute() }