Package: awstin
awstinHigh-level utilities for building and testing AWS applications in Python.DynamoDBProductionTo use DynamoDB either theTEST_DYNAMODB_ENDPOINT(for integration
testing) orAWS_REGION(for production) environment variable must be set.DynamoDB is accessed through Python data models that users define to represent
structured data in tables.fromawstin.dynamodbimportAttr,DynamoModel,KeyclassUser(DynamoModel):# Name of the DynamoDB table (required!)_table_name_="Users"# Sort or hash keys are marked with Keyuser_id=Key()# Other attributes are marked with Attrfavorite_color=Attr()# The names of attributes and keys can differ from the names on the data# model - the name of the attribute in DynamoDB should be passed to Attraccount_age=Attr("ageDays")Tables are tied to these data models. They'll be returned when items are
retrieved from the table. Also,put_itemtakes instances of this data model class.These data models also define projection expressions, so only those attributes
are retrieved fromget_item,query, andscancalls.fromawstin.dynamodbimportDynamoDBdynamodb=DynamoDB()# List of available tablestables=dynamodb.list_tables()# Access a table by modelusers_table=dynamodb[User]# Put an item into the tableuser=User(user_id="user123",favorite_color="Blue",account_age=120,)users_table.put_item(user)# Tables that only have a partition key can be accessed directly by their# partition keyitem1=users_table["user123"]# Tables that have partition and sort keys can be accessed by a tupletable2=dynamodb[AnotherTableModel]item2=table2[("hashval",123)]# Full primary key access is also availableitem3=table2[{"hashkey_name":"hashval","sortkey_name":123}]Query and scan filters can be built up using these data models as well. Results can be iterated without worrying about pagination.Table.scanandTable.queryyield items, requesting another page of items lazily only when it's out of items in a page.scan_filter=((User.account_age>30)&(User.favorite_color.in_(["Blue","Green"])))foruserinusers_table.scan(scan_filter):ban_user(user)Queries must be given a query expression and can optionally be given a filter expression. Query expressions must represent valid DynamoDB queries.classStudent(DynamoModel):_table_name_="Students"# Hash keyname=Key()# Sort keyyear=Key()homeroom=Attr()students_table=dynamodb[Student]query_expression=(Student.name=="John")&(Student.year>=10)filter_expression=Student.homeroom=="Smith"results=students_table.query(query_expression=query_expression,filter_expression=filter_expression,)Indexes work identically, but must have a_index_name_attribute on the data
model. Indexes can be used for queries and scans.classByHomeroomIndex(DynamoModel):_table_name_="Students"_index_name_="ByHomeroom"# Hash keyhomeroom=Key()# Sort keyname=Key()year=Attr()homeroom_index=dynamodb[ByHomeroomIndex]query_expression=((ByHomeroomIndex.homeroom=="Doe")&(ByHomeroomIndex.name>"B"))filter_expression=ByHomeroomIndex.year>11items=list(homeroom_index.query(query_expression,filter_expression))Nested ValuesFilters on nested attributes work as well:scan_filter=((MyModel.map_attr.key=="value")&(MyModel.list_attr[3]==10))results=my_table.scan(scan_filter)Updating ItemsA syntax is also available for updating items, with an optional condition expression:update_expression=(MyModel.an_attr.set(5-MyModel.another_attr)&MyModel.third_attr.add(100)&MyModel.another_attr.remove()&MyModel.set_attr.delete([2,3]))condition_expression=MyModel.an_attr>11updated=my_table.update_item("primary_key",update_expression,condition_expression,)if_not_existsandlist_appendare provided as well:fromawstin.dynamodbimportlist_appendupdate_expression=(MyModel.an_attr.set(MyModel.an_attr.if_not_exists(MyModel.another_attr))&MyModel.third_attr.set(list_append([1.1,2.2],MyModel.list_attr)))update_itemreturnsNoneif the condition evaluates toFalse.Float and DecimalFloats should be used when working with DynamoDB throughawstin. Conversions between float and Decimal is done internally.Unset ValuesValues in a data model class that are unset, either by user instantiation or by
retrieval from DynamoDB, are given the valueawstin.dynamodb.NOT_SET.TestingFor integration testing, a context manager to create and then automatically tear-down a DynamoDB table is provided.
The context manager waits for the table to be created/deleted before entering/exiting to avoid testing issues.
Hashkey and sortkey info can be provided.fromawstin.dynamodb.testingimporttemporary_dynamodb_tablewithtemporary_dynamodb_table(User,"hashkey_name")astable:item=User(user_id="user456",favorite_color="Green",account_age=333,)table.put_item(item)LambdasProductionLambda handlers can be made more readable by separating event parsing from business logic.
Thelambda_handlerdecorator factory takes a parser for the triggering event and context, and returns individual values to be used in the wrapped function.fromawstin.awslambdaimportlambda_handlerdefevent_parser(event,context):request_id=event["requestContext"]["requestId"]memory_limit=context["memory_limit_in_mb"]returnrequest_id,memory_limit@lambda_handler(event_parser)defhandle_custom_event(request_id,memory_limit):print(request_id)print(memory_limit)TestingA function wrapped withlambda_handleris stored on theinnerattribute of the returned function. That way, the business logic of the handler can be tested separately without having to build events.@lambda_handler(my_parser)defmy_handler(a:int,b:str):...# ------deftest_parser():args=my_parser(test_event,test_context)assert...deftest_handler():result=my_handler.inner(1,"abc")assert...API GatewayAuthorization LambdasProductionAuthorizor lambda responses can be generated with helper functions provided byawstin.apigateway.auth.accept,reject,unauthorized, andinvalidwill produce properly formatted auth lambda responses.fromawstin.apigatewayimportauthdefauth_event_parser(event,_context):token=event["headers"]["AuthToken"]resource_arn=event["methodArn"]principal_id=event["requestContext"]["connectionId"]returntoken,resource_arn,principal_id@lambda_handler(auth_event_parser)deftoken_auth(token,resource_arn,principal_id):iftoken=="good token":returnauth.accept(principal_id,resource_arn)eliftoken=="bad token":returnauth.reject(principal_id,resource_arn)eliftoken=="unauthorized token":returnauth.unauthorized()else:returnauth.invalid()WebsocketsProductionWebsocket pushes can be performed with a callback URL and message:fromawstin.apigateway.websocketimportWebsocketWebsocket("endpoint_url","dev").send("callback_url","message")SNSProductionSNS topics can be retrieved by name and published to with the message directly.
This requires either theTEST_SNS_ENDPOINT(for integration testing) orAWS_REGION(for production) environment variable to be set.fromawstin.snsimportSNSTopictopic=SNSTopic("topic-name")message_id=topic.publish("a message")Message attributes can be set from the kwargs of the publish:topic.publish("another message",attrib_a="a string",attrib_b=1234,attrib_c=["a","b",False,None],attrib_d=b"bytes value",)