diff --git a/src/carts/src/carts-service/app.py b/src/carts/src/carts-service/app.py index 4ab22c731..4b1211679 100644 --- a/src/carts/src/carts-service/app.py +++ b/src/carts/src/carts-service/app.py @@ -2,13 +2,14 @@ # SPDX-License-Identifier: MIT-0 import logging +# Set up logging +logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()]) import os from server import app from handlers import handler_bp from routes import route_bp -# Set up logging -logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()]) + app.register_blueprint(handler_bp) app.register_blueprint(route_bp) # Log a message at the start of the script diff --git a/src/docker-compose.yml b/src/docker-compose.yml index c87ace713..c928330ed 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -64,8 +64,6 @@ services: - WEB_ROOT_URL build: context: ./products - args: - - GOPROXY_OVERRIDE networks: - dev-net ports: diff --git a/src/products/Dockerfile b/src/products/Dockerfile index 7e89d1863..62db70479 100644 --- a/src/products/Dockerfile +++ b/src/products/Dockerfile @@ -2,19 +2,19 @@ FROM public.ecr.aws/docker/library/python:3.11-slim-bullseye WORKDIR /src/ -RUN apt-get update && apt-get install -y g++ +RUN apt-get update && apt-get install -y && rm -rf /var/lib/apt/lists/* COPY src/products-service/requirements.txt . +RUN python3 -m pip install --no-cache-dir -r requirements.txt -RUN python3 -m pip install -r requirements.txt -COPY src/products-service/server.py . -COPY src/products-service/app.py . -COPY src/products-service/routes.py . -COPY src/products-service/services.py . -COPY src/products-service/aws.py . +COPY src/products-service/ . + +RUN mkdir dynamo-data +COPY src/products-service/data/*.yaml dynamo-data/ EXPOSE 80 -ENTRYPOINT ["python"] -CMD ["app.py"] \ No newline at end of file +ENV PYTHONUNBUFFERED=0 + +CMD ["python", "app.py"] diff --git a/src/products/Dockerfile-go b/src/products/Dockerfile-go deleted file mode 100644 index 2ae34679f..000000000 --- a/src/products/Dockerfile-go +++ /dev/null @@ -1,17 +0,0 @@ -FROM public.ecr.aws/s5z3t2n9/golang:1.15-alpine AS build -ARG GOPROXY_OVERRIDE=https://proxy.golang.org -WORKDIR /src/ -RUN apk add --no-cache git bash -RUN go get -u github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute -COPY src/products-service/*.* /src/ -COPY src/products-service/data/*.* /src/data/ -RUN echo "Setting GOPROXY to $GOPROXY_OVERRIDE" -RUN go env -w GOPROXY=$GOPROXY_OVERRIDE -RUN CGO_ENABLED=0 go build -o /bin/products-service -RUN apk add ca-certificates -FROM scratch -COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=build /bin/products-service /bin/products-service -COPY --from=build /src/data/*.* /bin/data/ -EXPOSE 80 -ENTRYPOINT ["/bin/products-service"] \ No newline at end of file diff --git a/src/products/load_catalog.py b/src/products/load_catalog.py index 215d7569a..228c2d80b 100644 --- a/src/products/load_catalog.py +++ b/src/products/load_catalog.py @@ -156,7 +156,7 @@ def verify_local_ddb_running(endpoint,dynamodb): print(f'Usage: {sys.argv[0]} --categories-table-name CATEGORIES_TABLE_NAME [--categories-file CATEGORIES_FILE] --products-table-name PRODUCTS_TABLE_NAME [--products_file PRODUCTS_FILE] [--truncate] --carts-table-name CARTS_TABLE_NAME [--carts_file CARTS_FILE] [--endpoint-url ENDPOINT_URL]') sys.exit(1) - dynamodb = resource('dynamodb', endpoint_url=endpoint_url) + dynamodb = resource('dynamodb', endpoint_url=endpoint_url, region_name = 'us-west-2') if categories_table_name: print(f'Loading categories from {categories_file} into table {categories_table_name}') diff --git a/src/products/src/products-service/app.py b/src/products/src/products-service/app.py index d7604da34..49959dc51 100644 --- a/src/products/src/products-service/app.py +++ b/src/products/src/products-service/app.py @@ -1,18 +1,15 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -import logging import os from server import app +from routes import route_bp -# Set up logging -logging.basicConfig(filename='app.log', level=logging.INFO) +app.register_blueprint(route_bp) # Log a message at the start of the script app.logger.info('Starting app.py') -import routes - if __name__ == '__main__': try: port = os.getenv('PORT', '80') @@ -20,4 +17,4 @@ except Exception as e: # Log the error message and the type of the exception app.logger.error(f'Error starting server: {str(e)}') - app.logger.error(f'Type of error: {type(e)}') \ No newline at end of file + app.logger.error(f'Type of error: {type(e)}') diff --git a/src/products/src/products-service/aws.go b/src/products/src/products-service/aws.go deleted file mode 100644 index f39562f76..000000000 --- a/src/products/src/products-service/aws.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -import ( - "log" - "os" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/dynamodb" -) - -var sess, err = session.NewSession(&aws.Config{}) - -// DynamoDB table names passed via environment -var ddbTableProducts = os.Getenv("DDB_TABLE_PRODUCTS") -var ddbTableCategories = os.Getenv("DDB_TABLE_CATEGORIES") - -// Allow DDB endpoint to be overridden to support amazon/dynamodb-local -var ddbEndpointOverride = os.Getenv("DDB_ENDPOINT_OVERRIDE") -var runningLocal bool - -var dynamoClient *dynamodb.DynamoDB - -// Initialize clients -func init() { - if len(ddbEndpointOverride) > 0 { - runningLocal = true - log.Println("Creating DDB client with endpoint override: ", ddbEndpointOverride) - creds := credentials.NewStaticCredentials("does", "not", "matter") - awsConfig := &aws.Config{ - Credentials: creds, - Region: aws.String("us-east-1"), - Endpoint: aws.String(ddbEndpointOverride), - } - dynamoClient = dynamodb.New(sess, awsConfig) - } else { - runningLocal = false - dynamoClient = dynamodb.New(sess) - } -} diff --git a/src/products/src/products-service/category.go b/src/products/src/products-service/category.go deleted file mode 100644 index 7a36320b6..000000000 --- a/src/products/src/products-service/category.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -// Category Struct -// IMPORTANT: if you change the shape of this struct, be sure to update the retaildemostore-lambda-load-products Lambda too! -type Category struct { - ID string `json:"id" yaml:"id"` - URL string `json:"url" yaml:"url"` - Name string `json:"name" yaml:"name"` - Image string `json:"image" yaml:"image"` - HasGenderAffinity bool `json:"has_gender_affinity" yaml:"has_gender_affinity"` -} - -// Initialized - indicates if instance has been initialized or not -func (c *Category) Initialized() bool { return c != nil && len(c.ID) > 0 } - -// Categories Array -type Categories []Category diff --git a/src/products/src/products-service/aws.py b/src/products/src/products-service/dynamo_setup.py similarity index 99% rename from src/products/src/products-service/aws.py rename to src/products/src/products-service/dynamo_setup.py index b6fdc8e00..5abef52a1 100644 --- a/src/products/src/products-service/aws.py +++ b/src/products/src/products-service/dynamo_setup.py @@ -59,4 +59,4 @@ def setup(): region = 'us-east-1' dynamo_client = boto3.client('dynamodb', region_name=region) -setup() +setup() \ No newline at end of file diff --git a/src/products/src/products-service/dynamo_setup2.py b/src/products/src/products-service/dynamo_setup2.py new file mode 100644 index 000000000..5f97af02d --- /dev/null +++ b/src/products/src/products-service/dynamo_setup2.py @@ -0,0 +1,181 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +import os +import time +import boto3 + +from server import app +import yaml +from decimal import Decimal + +# DynamoDB table names passed via environment +ddb_table_products = os.getenv("DDB_TABLE_PRODUCTS") +ddb_table_categories = os.getenv("DDB_TABLE_CATEGORIES") +products_file = "dynamo-data/products.yaml" +categories_file = "dynamo-data/categories.yaml" + +def create_table(resource, ddb_table_name, attribute_definitions, key_schema, global_secondary_indexes=None): + try: + resource.create_table( + TableName=ddb_table_name, + KeySchema=key_schema, + AttributeDefinitions=attribute_definitions, + GlobalSecondaryIndexes=global_secondary_indexes or [], + BillingMode="PAY_PER_REQUEST", + ) + app.logger.info(f'Created table: {ddb_table_name}') + except Exception as e: + if e.response["Error"]["Code"] == "ResourceInUseException": + app.logger.info(f'Table {ddb_table_name} already exists; continuing...') + else: + raise e + +def verify_local_ddb_running(endpoint, dynamo_resource): + app.logger.info(f"Verifying that local DynamoDB is running at: {endpoint}") + for _ in range(5): + try: + response = dynamo_resource.meta.client.list_tables() + if ddb_table_products not in response['TableNames']: + try: + create_table( + ddb_table_name=ddb_table_products, + resource=dynamo_resource, + attribute_definitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "category", "AttributeType": "S"}, + {"AttributeName": "featured", "AttributeType": "S"}, + ], + key_schema=[ + {"AttributeName": "id", "KeyType": "HASH"}, + ], + global_secondary_indexes=[ + { + "IndexName": "category-index", + "KeySchema": [{"AttributeName": "category", "KeyType": "HASH"}], + "Projection": {"ProjectionType": "ALL"}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + } + }, + { + "IndexName": "featured-index", + "KeySchema": [{"AttributeName": "featured", "KeyType": "HASH"}], + "Projection": {"ProjectionType": "ALL"}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + } + } + ] + ) + table = dynamo_resource.Table(ddb_table_products) + app.logger.info(f'Loading products from {products_file}') + with open(products_file, 'r') as f: + products = yaml.safe_load(f) + app.logger.info(f'Updating products in table {ddb_table_products}') + for product in products: + product['id'] = str(product['id']) + if product.get('price'): + product['price'] = Decimal(str(product['price'])) + if product.get('featured'): + product['featured'] = str(product['featured']).lower() + table.put_item(Item=product) + + table.load() + app.logger.info(f"Table Name: {table.table_name}") + app.logger.info(f"Key Schema: {table.key_schema}") + app.logger.info(f"Attribute Definitions: {table.attribute_definitions}") + app.logger.info(f"Provisioned Throughput: {table.provisioned_throughput}") + app.logger.info(f"Global Secondary Indexes: {getattr(table, 'global_secondary_indexes', 'None')}") + app.logger.info(f"Local Secondary Indexes: {getattr(table, 'local_secondary_indexes', 'None')}") + app.logger.info(f"Table Status: {table.table_status}") + app.logger.info(f"Item Count: {table.item_count}") + app.logger.info(f"Table Size (Bytes): {table.table_size_bytes}") + app.logger.info(f"Creation Date Time: {table.creation_date_time.isoformat()}") + + app.logger.info(f'Products loaded: {len(products)}') + except Exception as e: + app.logger.error(f"Failed to initialize product table: {str(e)}") + exit(1) + if ddb_table_categories not in response['TableNames']: + try: + create_table( + resource=dynamo_resource, + ddb_table_name=ddb_table_categories, + attribute_definitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "name", "AttributeType": "S"}, + ], + key_schema=[ + {"AttributeName": "id", "KeyType": "HASH"}, + ], + global_secondary_indexes=[ + { + "IndexName": "name-index", + "KeySchema": [{"AttributeName": "name", "KeyType": "HASH"}], + "Projection": {"ProjectionType": "ALL"}, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + } + } + ] + ) + table = dynamo_resource.Table(ddb_table_categories) + app.logger.info(f'Loading categories from {categories_file}') + with open(categories_file, 'r') as f: + categories = yaml.safe_load(f) + app.logger.info(f'Updating categories in table {ddb_table_categories}') + for category in categories: + category['id'] = str(category['id']) + table.put_item(Item=category) + table.load() + app.logger.info(f"Table Name: {table.table_name}") + app.logger.info(f"Key Schema: {table.key_schema}") + app.logger.info(f"Attribute Definitions: {table.attribute_definitions}") + app.logger.info(f"Provisioned Throughput: {table.provisioned_throughput}") + app.logger.info(f"Global Secondary Indexes: {getattr(table, 'global_secondary_indexes', 'None')}") + app.logger.info(f"Local Secondary Indexes: {getattr(table, 'local_secondary_indexes', 'None')}") + app.logger.info(f"Table Status: {table.table_status}") + app.logger.info(f"Item Count: {table.item_count}") + app.logger.info(f"Table Size (Bytes): {table.table_size_bytes}") + app.logger.info(f"Creation Date Time: {table.creation_date_time.isoformat()}") + + app.logger.info(f'Categories loaded: {len(categories)}') + except Exception as e: + app.logger.error(f"Failed to initialize category table: {str(e)}") + exit(1) + app.logger.info("DynamoDB local is responding!") + return + except Exception as e: + app.logger.info(e) + app.logger.info("Local DynamoDB service is not ready yet... pausing before trying again") + time.sleep(2) + app.logger.info("Local DynamoDB service not responding; verify that your docker-compose .env file is set up correctly") + exit(1) + +ddb_endpoint_override = os.getenv("DDB_ENDPOINT_OVERRIDE") +running_local = False +dynamo_resource = None + +def setup(): + global dynamo_resource, running_local + + if ddb_endpoint_override: + running_local = True + app.logger.info("Creating DDB client with endpoint override: " + ddb_endpoint_override) + dynamo_resource = boto3.resource( + 'dynamodb', + endpoint_url=ddb_endpoint_override, + region_name='us-west-2', + aws_access_key_id='XXXX', + aws_secret_access_key='XXXX' + ) + verify_local_ddb_running(ddb_endpoint_override, dynamo_resource) + else: + running_local = False + dynamo_client = boto3.client('dynamodb') + +setup() diff --git a/src/products/src/products-service/go.mod b/src/products/src/products-service/go.mod deleted file mode 100644 index d0bd04ab6..000000000 --- a/src/products/src/products-service/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module products - -go 1.15 - -require ( - github.com/aws/aws-sdk-go v1.40.38 - github.com/google/uuid v1.1.5 - github.com/gorilla/mux v1.8.0 - gopkg.in/yaml.v2 v2.4.0 -) diff --git a/src/products/src/products-service/go.sum b/src/products/src/products-service/go.sum deleted file mode 100644 index 3108aaf13..000000000 --- a/src/products/src/products-service/go.sum +++ /dev/null @@ -1,29 +0,0 @@ -github.com/aws/aws-sdk-go v1.40.38 h1:kl3iIW0h/JEBFjSBcAxDsiRbKMPz4aI5FJIHMCAQ+J0= -github.com/aws/aws-sdk-go v1.40.38/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= -github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/src/products/src/products-service/handlers.go b/src/products/src/products-service/handlers.go deleted file mode 100644 index 810a39ee7..000000000 --- a/src/products/src/products-service/handlers.go +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "log" - "net/http" - "os" - - "github.com/gorilla/mux" - - "strconv" - "strings" -) - -var imageRootURL = os.Getenv("IMAGE_ROOT_URL") -var missingImageFile = "product_image_coming_soon.png" - -// initResponse -func initResponse(w *http.ResponseWriter) { - (*w).Header().Set("Access-Control-Allow-Origin", "*") - (*w).Header().Set("Content-Type", "application/json; charset=UTF-8") -} - -func fullyQualifyImageURLs(r *http.Request) bool { - param := r.URL.Query().Get("fullyQualifyImageUrls") - if len(param) == 0 { - param = "1" - } - - fullyQualify, _ := strconv.ParseBool(param) - return fullyQualify -} - -// fullyQualifyCategoryImageURL - fully qualifies image URL for a category -func fullyQualifyCategoryImageURL(r *http.Request, c *Category) { - if fullyQualifyImageURLs(r) { - if len(c.Image) > 0 && c.Image != missingImageFile { - c.Image = imageRootURL + c.Name + "/" + c.Image - } else { - c.Image = imageRootURL + missingImageFile - } - } else if len(c.Image) == 0 || c.Image == missingImageFile { - c.Image = missingImageFile - } -} - -// fullyQualifyCategoryImageURLs - fully qualifies image URL for categories -func fullyQualifyCategoryImageURLs(r *http.Request, categories *Categories) { - for i := range *categories { - category := &((*categories)[i]) - fullyQualifyCategoryImageURL(r, category) - } -} - -// fullyQualifyProductImageURL - fully qualifies image URL for a product -func fullyQualifyProductImageURL(r *http.Request, p *Product) { - if fullyQualifyImageURLs(r) { - if len(p.Image) > 0 && p.Image != missingImageFile { - p.Image = imageRootURL + p.Category + "/" + p.Image - } else { - p.Image = imageRootURL + missingImageFile - } - } else if len(p.Image) == 0 || p.Image == missingImageFile { - p.Image = missingImageFile - } -} - -// fullyQualifyProductImageURLs - fully qualifies image URLs for all products -func fullyQualifyProductImageURLs(r *http.Request, products *Products) { - for i := range *products { - product := &((*products)[i]) - fullyQualifyProductImageURL(r, product) - } -} - -// Index Handler -func Index(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "Welcome to the Products Web Service") -} - -// ProductIndex Handler -func ProductIndex(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - ret := RepoFindALLProducts() - - fullyQualifyProductImageURLs(r, &ret) - - if err := json.NewEncoder(w).Encode(ret); err != nil { - panic(err) - } -} - -// CategoryIndex Handler -func CategoryIndex(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - ret := RepoFindALLCategories() - - fullyQualifyCategoryImageURLs(r, &ret) - - // TODO - if err := json.NewEncoder(w).Encode(ret); err != nil { - panic(err) - } -} - -// ProductShow Handler -func ProductShow(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - vars := mux.Vars(r) - - productIds := strings.Split(vars["productIDs"], ",") - - if len(productIds) > MAX_BATCH_GET_ITEM { - http.Error(w, fmt.Sprintf("Maximum number of product IDs per request is %d", MAX_BATCH_GET_ITEM), http.StatusUnprocessableEntity) - return - } - - if len(productIds) > 1 { - ret := RepoFindMultipleProducts(productIds) - - fullyQualifyProductImageURLs(r, &ret) - - if err := json.NewEncoder(w).Encode(ret); err != nil { - panic(err) - } - } else { - ret := RepoFindProduct(productIds[0]) - - if !ret.Initialized() { - http.Error(w, "Product not found", http.StatusNotFound) - return - } - - fullyQualifyProductImageURL(r, &ret) - - if err := json.NewEncoder(w).Encode(ret); err != nil { - panic(err) - } - } -} - -// CategoryShow Handler -func CategoryShow(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - vars := mux.Vars(r) - - ret := RepoFindCategory(vars["categoryID"]) - - if !ret.Initialized() { - http.Error(w, "Category not found", http.StatusNotFound) - return - } - - fullyQualifyCategoryImageURL(r, &ret) - - if err := json.NewEncoder(w).Encode(ret); err != nil { - panic(err) - } -} - -// ProductInCategory Handler -func ProductInCategory(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - vars := mux.Vars(r) - categoryName := vars["categoryName"] - - ret := RepoFindProductByCategory(categoryName) - - fullyQualifyProductImageURLs(r, &ret) - - if err := json.NewEncoder(w).Encode(ret); err != nil { - panic(err) - } -} - -// ProductFeatured Handler -func ProductFeatured(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - ret := RepoFindFeatured() - - fullyQualifyProductImageURLs(r, &ret) - - if err := json.NewEncoder(w).Encode(ret); err != nil { - panic(err) - } -} - -func validateProduct(product *Product) error { - if len(product.Name) == 0 { - return errors.New("Product name is required") - } - - if product.Price < 0 { - return errors.New("Product price cannot be a negative value") - } - - if product.CurrentStock < 0 { - return errors.New("Product current stock cannot be a negative value") - } - - if len(product.Category) > 0 { - categories := RepoFindCategoriesByName(product.Category) - if len(categories) == 0 { - return errors.New("Invalid product category; does not exist") - } - } - - return nil -} - -// UpdateProduct - updates a product -func UpdateProduct(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - vars := mux.Vars(r) - - print(vars) - var product Product - - body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) - if err != nil { - panic(err) - } - if err := r.Body.Close(); err != nil { - panic(err) - } - if err := json.Unmarshal(body, &product); err != nil { - http.Error(w, "Invalid request payload", http.StatusUnprocessableEntity) - if err := json.NewEncoder(w).Encode(err); err != nil { - panic(err) - } - } - - if err := validateProduct(&product); err != nil { - http.Error(w, err.Error(), http.StatusUnprocessableEntity) - return - } - - existingProduct := RepoFindProduct(vars["productID"]) - if !existingProduct.Initialized() { - // Existing product does not exist - http.Error(w, "Product not found", http.StatusNotFound) - return - } - - if err := RepoUpdateProduct(&existingProduct, &product); err != nil { - http.Error(w, "Internal error updating product", http.StatusInternalServerError) - return - } - - fullyQualifyProductImageURL(r, &product) - - if err := json.NewEncoder(w).Encode(product); err != nil { - panic(err) - } -} - -// UpdateInventory - updates stock quantity for one item -func UpdateInventory(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - vars := mux.Vars(r) - - var inventory Inventory - - body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) - if err != nil { - panic(err) - } - if err := r.Body.Close(); err != nil { - panic(err) - } - log.Println("UpdateInventory Body ", body) - - if err := json.Unmarshal(body, &inventory); err != nil { - http.Error(w, "Invalid request payload", http.StatusUnprocessableEntity) - if err := json.NewEncoder(w).Encode(err); err != nil { - panic(err) - } - } - - log.Println("UpdateInventory --> ", inventory) - - // Get the current product - product := RepoFindProduct(vars["productID"]) - if !product.Initialized() { - // Existing product does not exist - http.Error(w, "Product not found", http.StatusNotFound) - return - } - - if err := RepoUpdateInventoryDelta(&product, inventory.StockDelta); err != nil { - panic(err) - } - - fullyQualifyProductImageURL(r, &product) - - if err := json.NewEncoder(w).Encode(product); err != nil { - panic(err) - } -} - -// NewProduct - creates a new Product -func NewProduct(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - var product Product - body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) - if err != nil { - panic(err) - } - if err := r.Body.Close(); err != nil { - panic(err) - } - if err := json.Unmarshal(body, &product); err != nil { - http.Error(w, "Invalid request payload", http.StatusUnprocessableEntity) - if err := json.NewEncoder(w).Encode(err); err != nil { - panic(err) - } - } - - log.Println("NewProduct ", product) - - if err := validateProduct(&product); err != nil { - http.Error(w, err.Error(), http.StatusUnprocessableEntity) - return - } - - if err := RepoNewProduct(&product); err != nil { - http.Error(w, "Internal error creating product", http.StatusInternalServerError) - return - } - - fullyQualifyProductImageURL(r, &product) - - if err := json.NewEncoder(w).Encode(product); err != nil { - panic(err) - } -} - -// DeleteProduct - deletes a single product -func DeleteProduct(w http.ResponseWriter, r *http.Request) { - initResponse(&w) - - vars := mux.Vars(r) - - // Get the current product - product := RepoFindProduct(vars["productID"]) - if !product.Initialized() { - // Existing product does not exist - http.Error(w, "Product not found", http.StatusNotFound) - return - } - - if err := RepoDeleteProduct(&product); err != nil { - http.Error(w, "Internal error deleting product", http.StatusInternalServerError) - } -} diff --git a/src/products/src/products-service/handlers.py b/src/products/src/products-service/handlers.py new file mode 100644 index 000000000..ba5b9149c --- /dev/null +++ b/src/products/src/products-service/handlers.py @@ -0,0 +1,40 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +from flask import jsonify,Blueprint +from werkzeug.exceptions import BadRequest, UnsupportedMediaType, NotFound +from botocore.exceptions import BotoCoreError +from server import app + +handler_bp = Blueprint('handler_bp', __name__) + +@app.errorhandler(BadRequest) +def handle_bad_request(e): + app.logger.error(f'BadRequest: {str(e)}') + return jsonify({"error": "Bad request, please check your input"}), 400 + +@app.errorhandler(BotoCoreError) +def handle_boto_core_error(e): + app.logger.error(f'BotoCoreError: {str(e)}') + return jsonify({"error": "Internal server error"}), 500 + +#error for user not found +@app.errorhandler(NotFound) +def handle_not_found(e): + app.logger.error(f'NotFound: {str(e)}') + return jsonify({"error": "User not found"}), 404 + +@app.errorhandler(KeyError) +def handle_key_error(e): + app.logger.error(f'KeyError: {str(e)}') + return jsonify({"error": "Not found"}), 404 + +@app.errorhandler(UnsupportedMediaType) +def handle_unsupported_media_type(e): + app.logger.error(f'UnsupportedMediaType: {str(e)}') + return jsonify({"error": "Unsupported media type"}), 415 + +@app.errorhandler(500) +def handle_internal_error(e): + app.logger.error(f'InternalServerError: {str(e)}') + return jsonify({"error": "Internal server error"}), 500 \ No newline at end of file diff --git a/src/products/src/products-service/localdev.go b/src/products/src/products-service/localdev.go deleted file mode 100644 index 626e8d598..000000000 --- a/src/products/src/products-service/localdev.go +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -/* - * Supports developing locally where DDB is running locally using - * amazon/dynamodb-local (Docker) or local DynamoDB. - * https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html - */ - -package main - -import ( - "fmt" - "io/ioutil" - "log" - "net/http" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" - yaml "gopkg.in/yaml.v2" -) - -func init() { - if runningLocal { - waitForLocalDDB() - loadData() - } -} - -// waitForLocalDDB - since local DDB can take a couple seconds to startup, we give it some time. -func waitForLocalDDB() { - log.Println("Verifying that local DynamoDB is running at: ", ddbEndpointOverride) - - ddbRunning := false - - for i := 0; i < 5; i++ { - resp, _ := http.Get(ddbEndpointOverride) - - if resp != nil && resp.StatusCode >= 200 { - log.Println("Received HTTP response from local DynamoDB service!") - ddbRunning = true - break - } - - log.Println("Local DynamoDB service is not ready yet... pausing before trying again") - time.Sleep(2 * time.Second) - } - - if !ddbRunning { - log.Panic("Local DynamoDB service not responding; verify that your docker-compose .env file is setup correctly") - } -} - -func loadData() { - err := createProductsTable() - if err != nil { - log.Panic("Unable to create products table.") - } - - err = loadProducts("/bin/data/products.yaml") - if err != nil { - log.Panic("Unable to load products file.") - } - - err = createCategoriesTable() - if err != nil { - log.Panic("Unable to create categories table.") - } - - err = loadCategories("/bin/data/categories.yaml") - if err != nil { - log.Panic("Unable to load category file.") - } - - log.Println("Successfully loaded product and category data into DDB") -} - -func createProductsTable() error { - log.Println("Creating products table: ", ddbTableProducts) - - // Table definition mapped from /aws/cloudformation-templates/base/tables.yaml - input := &dynamodb.CreateTableInput{ - AttributeDefinitions: []*dynamodb.AttributeDefinition{ - { - AttributeName: aws.String("id"), - AttributeType: aws.String("S"), - }, - { - AttributeName: aws.String("category"), - AttributeType: aws.String("S"), - }, - { - AttributeName: aws.String("featured"), - AttributeType: aws.String("S"), - }, - }, - KeySchema: []*dynamodb.KeySchemaElement{ - { - AttributeName: aws.String("id"), - KeyType: aws.String("HASH"), - }, - }, - BillingMode: aws.String("PAY_PER_REQUEST"), - GlobalSecondaryIndexes: []*dynamodb.GlobalSecondaryIndex{ - { - IndexName: aws.String("category-index"), - KeySchema: []*dynamodb.KeySchemaElement{ - { - AttributeName: aws.String("category"), - KeyType: aws.String("HASH"), - }, - }, - Projection: &dynamodb.Projection{ - ProjectionType: aws.String("ALL"), - }, - }, - { - IndexName: aws.String("featured-index"), - KeySchema: []*dynamodb.KeySchemaElement{ - { - AttributeName: aws.String("featured"), - KeyType: aws.String("HASH"), - }, - }, - Projection: &dynamodb.Projection{ - ProjectionType: aws.String("ALL"), - }, - }, - }, - TableName: aws.String(ddbTableProducts), - } - - _, err := dynamoClient.CreateTable(input) - if err != nil { - log.Println("Error creating products table: ", ddbTableProducts) - - if aerr, ok := err.(awserr.Error); ok { - if aerr.Code() == dynamodb.ErrCodeResourceInUseException { - log.Println("Table already exists; continuing") - err = nil - } else { - log.Println(err.Error()) - } - } else { - log.Println(err.Error()) - } - } - - return err -} - -func loadProducts(filename string) error { - start := time.Now() - - log.Println("Loading products from file: ", filename) - - var r Products - - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return err - } - - err = yaml.Unmarshal(bytes, &r) - if err != nil { - return err - } - - for _, item := range r { - - av, err := dynamodbattribute.MarshalMap(item) - - if err != nil { - return err - } - - input := &dynamodb.PutItemInput{ - Item: av, - TableName: aws.String(ddbTableProducts), - } - - _, err = dynamoClient.PutItem(input) - if err != nil { - fmt.Println("Got error calling PutItem:") - fmt.Println(err.Error()) - - } - - } - - log.Println("Products loaded in ", time.Since(start)) - - return nil -} - -func createCategoriesTable() error { - log.Println("Creating categories table: ", ddbTableCategories) - - // Table definition mapped from /aws/cloudformation-templates/base/tables.yaml - input := &dynamodb.CreateTableInput{ - AttributeDefinitions: []*dynamodb.AttributeDefinition{ - { - AttributeName: aws.String("id"), - AttributeType: aws.String("S"), - }, - { - AttributeName: aws.String("name"), - AttributeType: aws.String("S"), - }, - }, - KeySchema: []*dynamodb.KeySchemaElement{ - { - AttributeName: aws.String("id"), - KeyType: aws.String("HASH"), - }, - }, - BillingMode: aws.String("PAY_PER_REQUEST"), - GlobalSecondaryIndexes: []*dynamodb.GlobalSecondaryIndex{ - { - IndexName: aws.String("name-index"), - KeySchema: []*dynamodb.KeySchemaElement{ - { - AttributeName: aws.String("name"), - KeyType: aws.String("HASH"), - }, - }, - Projection: &dynamodb.Projection{ - ProjectionType: aws.String("ALL"), - }, - }, - }, - TableName: aws.String(ddbTableCategories), - } - - _, err := dynamoClient.CreateTable(input) - if err != nil { - log.Println("Error creating categories table: ", ddbTableCategories) - - if aerr, ok := err.(awserr.Error); ok { - if aerr.Code() == dynamodb.ErrCodeResourceInUseException { - log.Println("Table already exists; continuing") - err = nil - } else { - log.Println(err.Error()) - } - } else { - log.Println(err.Error()) - } - } - - return err -} - -func loadCategories(filename string) error { - - start := time.Now() - - log.Println("Loading categories from file: ", filename) - - var r Categories - - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return err - } - - err = yaml.Unmarshal(bytes, &r) - if err != nil { - return err - } - for _, item := range r { - av, err := dynamodbattribute.MarshalMap(item) - - if err != nil { - return err - } - - input := &dynamodb.PutItemInput{ - Item: av, - TableName: aws.String(ddbTableCategories), - } - - _, err = dynamoClient.PutItem(input) - if err != nil { - fmt.Println("Got error calling PutItem:") - fmt.Println(err.Error()) - } - } - - log.Println("Categories loaded in ", time.Since(start)) - - return nil -} diff --git a/src/products/src/products-service/logger.go b/src/products/src/products-service/logger.go deleted file mode 100644 index 49a3a9102..000000000 --- a/src/products/src/products-service/logger.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -import ( - "log" - "net/http" - "time" -) - -// loggingResponseWriter Struct -type loggingResponseWriter struct { - http.ResponseWriter - statusCode int -} - -// NewLoggingResponseWriter Function -func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { - return &loggingResponseWriter{w, http.StatusOK} -} - -//WriteHeader Function -func (lrw *loggingResponseWriter) WriteHeader(code int) { - lrw.statusCode = code - lrw.ResponseWriter.WriteHeader(code) -} - -// Logger Function -func Logger(inner http.Handler, name string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - lrw := NewLoggingResponseWriter(w) - - inner.ServeHTTP(lrw, r) - - statusCode := lrw.statusCode - - // Log Response - log.Printf( - "%s\t%s\t%s\t%d\t%s", - r.Method, - r.RequestURI, - name, - statusCode, - time.Since(start), - ) - - }) -} diff --git a/src/products/src/products-service/main.go b/src/products/src/products-service/main.go deleted file mode 100644 index a807551e7..000000000 --- a/src/products/src/products-service/main.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -import ( - "log" - "net/http" -) - -func main() { - router := NewRouter() - log.Fatal(http.ListenAndServe(":80", router)) -} diff --git a/src/products/src/products-service/product.go b/src/products/src/products-service/product.go deleted file mode 100644 index c5f6fdc00..000000000 --- a/src/products/src/products-service/product.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -// Product Struct -// using omitempty as a DynamoDB optimization to create indexes -// IMPORTANT: if you change the shape of this struct, be sure to update the retaildemostore-lambda-load-products Lambda too! -type Product struct { - ID string `json:"id" yaml:"id"` - URL string `json:"url" yaml:"url"` - SK string `json:"sk" yaml:"sk"` - Name string `json:"name" yaml:"name"` - Category string `json:"category" yaml:"category"` - Style string `json:"style" yaml:"style"` - Description string `json:"description" yaml:"description"` - Aliases []string `json:"aliases" yaml:"aliases"` // keywords for use with e.g. Alexa - Price float32 `json:"price" yaml:"price"` - Image string `json:"image" yaml:"image"` - Featured string `json:"featured,omitempty" yaml:"featured,omitempty"` - GenderAffinity string `json:"gender_affinity,omitempty" yaml:"gender_affinity,omitempty"` - CurrentStock int `json:"current_stock" yaml:"current_stock"` - Promoted string `json:"promoted,omitempty" yaml:"promoted,omitempty"` -} - -// Initialized - indicates if instance has been initialized or not -func (p *Product) Initialized() bool { return p != nil && len(p.ID) > 0 } - -// Products Array -type Products []Product - -// Inventory Struct -type Inventory struct { - StockDelta int `json:"stock_delta" yaml:"stock_delta"` -} diff --git a/src/products/src/products-service/repository.go b/src/products/src/products-service/repository.go deleted file mode 100644 index 0793ae5cf..000000000 --- a/src/products/src/products-service/repository.go +++ /dev/null @@ -1,571 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -import ( - "fmt" - "log" - "os" - "strconv" - "strings" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" - "github.com/aws/aws-sdk-go/service/dynamodb/expression" - guuuid "github.com/google/uuid" -) - -// Root/base URL to use when building fully-qualified URLs to product detail view. -var webRootURL = os.Getenv("WEB_ROOT_URL") - -var MAX_BATCH_GET_ITEM = 100 - -func setProductURL(p *Product) { - if len(webRootURL) > 0 { - p.URL = webRootURL + "/#/product/" + p.ID - } -} - -func setCategoryURL(c *Category) { - if len(webRootURL) > 0 && len(c.Name) > 0 { - c.URL = webRootURL + "/#/category/" + c.Name - } -} - -// RepoFindProduct Function -func RepoFindProduct(id string) Product { - var product Product - - id = strings.ToLower(id) - - log.Println("RepoFindProduct: ", id, ddbTableProducts) - - result, err := dynamoClient.GetItem(&dynamodb.GetItemInput{ - TableName: aws.String(ddbTableProducts), - Key: map[string]*dynamodb.AttributeValue{ - "id": { - S: aws.String(id), - }, - }, - }) - - if err != nil { - log.Println("get item error " + string(err.Error())) - return product - } - - if result.Item != nil { - err = dynamodbattribute.UnmarshalMap(result.Item, &product) - - if err != nil { - panic(fmt.Sprintf("Failed to unmarshal Record, %v", err)) - } - - setProductURL(&product) - - log.Println("RepoFindProduct returning: ", product.Name, product.Category) - } - - return product -} - -// RepoFindMultipleProducts Function -func RepoFindMultipleProducts(ids []string) Products { - if len(ids) > MAX_BATCH_GET_ITEM { - panic(fmt.Sprintf("Failed to unmarshal Record, %d", MAX_BATCH_GET_ITEM)) - } - - var products Products - - mapOfAttrKeys := []map[string]*dynamodb.AttributeValue{} - - for _, id := range ids { - mapOfAttrKeys = append(mapOfAttrKeys, map[string]*dynamodb.AttributeValue{ - "id": &dynamodb.AttributeValue{ - S: aws.String(id), - }, - }) - log.Println(string(id)) - } - - input := &dynamodb.BatchGetItemInput{ - RequestItems: map[string]*dynamodb.KeysAndAttributes{ - ddbTableProducts: &dynamodb.KeysAndAttributes{ - Keys: mapOfAttrKeys, - }, - }, - } - - result, err := dynamoClient.BatchGetItem(input) - - if err != nil { - log.Println("BatchGetItem error " + string(err.Error())) - - return products - } - - var itemCount = 0 - - for _, table := range result.Responses { - for _, item := range table { - product := Product{} - - err = dynamodbattribute.UnmarshalMap(item, &product) - - if err != nil { - log.Println("Got error unmarshalling:") - log.Println(err.Error()) - } else { - setProductURL(&product) - } - - products = append(products, product) - itemCount += 1 - } - } - - if itemCount == 0 { - products = make([]Product, 0) - } - - return products -} - -// RepoFindCategory Function -func RepoFindCategory(id string) Category { - var category Category - - id = strings.ToLower(id) - - log.Println("RepoFindCategory: ", id, ddbTableCategories) - - result, err := dynamoClient.GetItem(&dynamodb.GetItemInput{ - TableName: aws.String(ddbTableCategories), - Key: map[string]*dynamodb.AttributeValue{ - "id": { - S: aws.String(id), - }, - }, - }) - - if err != nil { - log.Println("get item error " + string(err.Error())) - return category - } - - if result.Item != nil { - err = dynamodbattribute.UnmarshalMap(result.Item, &category) - - if err != nil { - panic(fmt.Sprintf("Failed to unmarshal Record, %v", err)) - } - - setCategoryURL(&category) - - log.Println("RepoFindCategory returning: ", category.Name) - } - - return category -} - -// RepoFindCategoriesByName Function -func RepoFindCategoriesByName(name string) Categories { - var categories Categories - - log.Println("RepoFindCategoriesByName: ", name, ddbTableCategories) - - keycond := expression.Key("name").Equal(expression.Value(name)) - proj := expression.NamesList(expression.Name("id"), - expression.Name("name"), - expression.Name("image")) - expr, err := expression.NewBuilder().WithKeyCondition(keycond).WithProjection(proj).Build() - - if err != nil { - log.Println("Got error building expression:") - log.Println(err.Error()) - } - - // Build the query input parameters - params := &dynamodb.QueryInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - KeyConditionExpression: expr.KeyCondition(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(ddbTableCategories), - IndexName: aws.String("name-index"), - } - // Make the DynamoDB Query API call - result, err := dynamoClient.Query(params) - - if err != nil { - log.Println("Got error QUERY expression:") - log.Println(err.Error()) - } - - log.Println("RepoFindCategoriesByName / items found = ", len(result.Items)) - - for _, i := range result.Items { - item := Category{} - - err = dynamodbattribute.UnmarshalMap(i, &item) - - if err != nil { - log.Println("Got error unmarshalling:") - log.Println(err.Error()) - } else { - setCategoryURL(&item) - } - - categories = append(categories, item) - } - - if len(result.Items) == 0 { - categories = make([]Category, 0) - } - - return categories -} - -// RepoFindProductByCategory Function -func RepoFindProductByCategory(category string) Products { - - log.Println("RepoFindProductByCategory: ", category) - - var f Products - - keycond := expression.Key("category").Equal(expression.Value(category)) - proj := expression.NamesList(expression.Name("id"), - expression.Name("category"), - expression.Name("name"), - expression.Name("image"), - expression.Name("style"), - expression.Name("description"), - expression.Name("price"), - expression.Name("gender_affinity"), - expression.Name("current_stock"), - expression.Name("promoted")) - expr, err := expression.NewBuilder().WithKeyCondition(keycond).WithProjection(proj).Build() - - if err != nil { - log.Println("Got error building expression:") - log.Println(err.Error()) - } - - // Build the query input parameters - params := &dynamodb.QueryInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - KeyConditionExpression: expr.KeyCondition(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(ddbTableProducts), - IndexName: aws.String("category-index"), - } - // Make the DynamoDB Query API call - result, err := dynamoClient.Query(params) - - if err != nil { - log.Println("Got error QUERY expression:") - log.Println(err.Error()) - } - - log.Println("RepoFindProductByCategory / items found = ", len(result.Items)) - - for _, i := range result.Items { - item := Product{} - - err = dynamodbattribute.UnmarshalMap(i, &item) - - if err != nil { - log.Println("Got error unmarshalling:") - log.Println(err.Error()) - } else { - setProductURL(&item) - } - - f = append(f, item) - } - - if len(result.Items) == 0 { - f = make([]Product, 0) - } - - return f -} - -// RepoFindFeatured Function -func RepoFindFeatured() Products { - - log.Println("RepoFindFeatured | featured=true") - - var f Products - - filt := expression.Name("featured").Equal(expression.Value("true")) - expr, err := expression.NewBuilder().WithFilter(filt).Build() - - if err != nil { - log.Println("Got error building expression:") - log.Println(err.Error()) - } - - // Build the query input - // using index for performance (few items are featured) - params := &dynamodb.ScanInput{ - ExpressionAttributeNames: expr.Names(), - ExpressionAttributeValues: expr.Values(), - FilterExpression: expr.Filter(), - ProjectionExpression: expr.Projection(), - TableName: aws.String(ddbTableProducts), - IndexName: aws.String("featured-index"), - } - // Make the DynamoDB Query API call - result, err := dynamoClient.Scan(params) - - if err != nil { - log.Println("Got error scan expression:") - log.Println(err.Error()) - } - - log.Println("RepoFindProductFeatured / items found = ", len(result.Items)) - - for _, i := range result.Items { - item := Product{} - - err = dynamodbattribute.UnmarshalMap(i, &item) - - if err != nil { - log.Println("Got error unmarshalling:") - log.Println(err.Error()) - } else { - setProductURL(&item) - } - - f = append(f, item) - } - - if len(result.Items) == 0 { - f = make([]Product, 0) - } - - return f -} - -// RepoFindALLCategories - loads all categories -func RepoFindALLCategories() Categories { - // TODO: implement some caching - - log.Println("RepoFindALLCategories: ") - - var f Categories - - // Build the query input parameters - params := &dynamodb.ScanInput{ - TableName: aws.String(ddbTableCategories), - } - // Make the DynamoDB Query API call - result, err := dynamoClient.Scan(params) - - if err != nil { - log.Println("Got error scan expression:") - log.Println(err.Error()) - } - - log.Println("RepoFindALLCategories / items found = ", len(result.Items)) - - for _, i := range result.Items { - item := Category{} - - err = dynamodbattribute.UnmarshalMap(i, &item) - - if err != nil { - log.Println("Got error unmarshalling:") - log.Println(err.Error()) - } else { - setCategoryURL(&item) - } - - f = append(f, item) - } - - if len(result.Items) == 0 { - f = make([]Category, 0) - } - - return f -} - -// RepoFindALLProducts Function -func RepoFindALLProducts() Products { - - log.Println("RepoFindALLProducts") - - var f Products - - // Build the query input parameters - params := &dynamodb.ScanInput{ - TableName: aws.String(ddbTableProducts), - } - // Make the DynamoDB Query API call - result, err := dynamoClient.Scan(params) - - if err != nil { - log.Println("Got error scan expression:") - log.Println(err.Error()) - } - - log.Println("RepoFindALLProducts / items found = ", len(result.Items)) - - for _, i := range result.Items { - item := Product{} - - err = dynamodbattribute.UnmarshalMap(i, &item) - - if err != nil { - log.Println("Got error unmarshalling:") - log.Println(err.Error()) - } else { - setProductURL(&item) - } - - f = append(f, item) - } - - if len(result.Items) == 0 { - f = make([]Product, 0) - } - - return f -} - -// RepoUpdateProduct - updates an existing product -func RepoUpdateProduct(existingProduct *Product, updatedProduct *Product) error { - updatedProduct.ID = existingProduct.ID // Ensure we're not changing product ID. - updatedProduct.URL = "" // URL is generated so ignore if specified - log.Printf("UpdateProduct from %#v to %#v", existingProduct, updatedProduct) - - av, err := dynamodbattribute.MarshalMap(updatedProduct) - - if err != nil { - fmt.Println("Got error calling dynamodbattribute MarshalMap:") - fmt.Println(err.Error()) - return err - } - - input := &dynamodb.PutItemInput{ - Item: av, - TableName: aws.String(ddbTableProducts), - } - - _, err = dynamoClient.PutItem(input) - if err != nil { - fmt.Println("Got error calling PutItem:") - fmt.Println(err.Error()) - } - - setProductURL(updatedProduct) - - return err -} - -// RepoUpdateInventoryDelta - updates a product's current inventory -func RepoUpdateInventoryDelta(product *Product, stockDelta int) error { - - log.Printf("RepoUpdateInventoryDelta for product %#v, delta: %v", product, stockDelta) - - if product.CurrentStock+stockDelta < 0 { - // ensuring we don't get negative stocks, just down to zero stock - // FUTURE: allow backorders via negative current stock? - stockDelta = -product.CurrentStock - } - - input := &dynamodb.UpdateItemInput{ - ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ - ":stock_delta": { - N: aws.String(strconv.Itoa(stockDelta)), - }, - ":currstock": { - N: aws.String(strconv.Itoa(product.CurrentStock)), - }, - }, - TableName: aws.String(ddbTableProducts), - Key: map[string]*dynamodb.AttributeValue{ - "id": { - S: aws.String(product.ID), - }, - "category": { - S: aws.String(product.Category), - }, - }, - ReturnValues: aws.String("UPDATED_NEW"), - UpdateExpression: aws.String("set current_stock = current_stock + :stock_delta"), - ConditionExpression: aws.String("current_stock = :currstock"), - } - - _, err = dynamoClient.UpdateItem(input) - if err != nil { - fmt.Println("Got error calling UpdateItem:") - fmt.Println(err.Error()) - } else { - product.CurrentStock = product.CurrentStock + stockDelta - } - - return err -} - -// RepoNewProduct - initializes and persists new product -func RepoNewProduct(product *Product) error { - log.Printf("RepoNewProduct --> %#v", product) - - if len(product.ID) == 0 { - product.ID = strings.ToLower(guuuid.New().String()) - } - av, err := dynamodbattribute.MarshalMap(product) - - if err != nil { - fmt.Println("Got error calling dynamodbattribute MarshalMap:") - fmt.Println(err.Error()) - return err - } - - input := &dynamodb.PutItemInput{ - Item: av, - TableName: aws.String(ddbTableProducts), - } - - _, err = dynamoClient.PutItem(input) - if err != nil { - fmt.Println("Got error calling PutItem:") - fmt.Println(err.Error()) - } - - setProductURL(product) - - return err -} - -// RepoDeleteProduct - deletes a single product -func RepoDeleteProduct(product *Product) error { - log.Println("Deleting product: ", product) - - input := &dynamodb.DeleteItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "id": { - S: aws.String(product.ID), - }, - "category": { - S: aws.String(product.Category), - }, - }, - TableName: aws.String(ddbTableProducts), - } - - _, err := dynamoClient.DeleteItem(input) - - if err != nil { - fmt.Println("Got error calling DeleteItem:") - fmt.Println(err.Error()) - } - - return err -} diff --git a/src/products/src/products-service/router.go b/src/products/src/products-service/router.go deleted file mode 100644 index 82678d68f..000000000 --- a/src/products/src/products-service/router.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -import ( - "net/http" - - "github.com/gorilla/mux" -) - -// NewRouter Router -func NewRouter() *mux.Router { - router := mux.NewRouter().StrictSlash(true) - for _, route := range routes { - var handler http.Handler - handler = route.HandlerFunc - handler = Logger(handler, route.Name) - - router. - Methods(route.Method). - Path(route.Pattern). - Name(route.Name). - Handler(handler) - - } - return router -} diff --git a/src/products/src/products-service/routes.go b/src/products/src/products-service/routes.go deleted file mode 100644 index 70aeff8c7..000000000 --- a/src/products/src/products-service/routes.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - -package main - -import "net/http" - -// Route Struct -type Route struct { - Name string - Method string - Pattern string - HandlerFunc http.HandlerFunc -} - -// Routes Array -type Routes []Route - -var routes = Routes{ - Route{ - "Index", - "GET", - "/", - Index, - }, - Route{ - "ProductIndex", - "GET", - "/products/all", - ProductIndex, - }, - Route{ - "ProductShow", - "GET", - "/products/id/{productIDs}", - ProductShow, - }, - Route{ - "ProductFeatured", - "GET", - "/products/featured", - ProductFeatured, - }, - Route{ - "ProductInCategory", - "GET", - "/products/category/{categoryName}", - ProductInCategory, - }, - Route{ - "ProductUpdate", - "PUT", - "/products/id/{productID}", - UpdateProduct, - }, - Route{ - "ProductDelete", - "DELETE", - "/products/id/{productID}", - DeleteProduct, - }, - Route{ - "NewProduct", - "POST", - "/products", - NewProduct, - }, - Route{ - "InventoryUpdate", - "PUT", - "/products/id/{productID}/inventory", - UpdateInventory, - }, - Route{ - "CategoryIndex", - "GET", - "/categories/all", - CategoryIndex, - }, - Route{ - "CategoryShow", - "GET", - "/categories/id/{categoryID}", - CategoryShow, - }, -} diff --git a/src/products/src/products-service/routes.py b/src/products/src/products-service/routes.py index 1c239cff3..84f8cb763 100644 --- a/src/products/src/products-service/routes.py +++ b/src/products/src/products-service/routes.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT-0 from decimal import Decimal -from flask import jsonify +from flask import jsonify,Blueprint from server import app from services import * from flask import request, abort, Response @@ -11,7 +11,7 @@ from werkzeug.exceptions import HTTPException import json - +route_bp = Blueprint('route_bp', __name__) product_service = ProductService() image_root_url = os.getenv('IMAGE_ROOT_URL') diff --git a/src/products/src/products-service/routes1.py b/src/products/src/products-service/routes1.py new file mode 100644 index 000000000..c06ac203a --- /dev/null +++ b/src/products/src/products-service/routes1.py @@ -0,0 +1,214 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +from decimal import Decimal +from flask import jsonify, Blueprint +from server import app +from services import * +from flask import request, abort, Response +from typing import List, Dict, Any +from http import HTTPStatus +from werkzeug.exceptions import HTTPException +import json + + +route_bp = Blueprint('route_bp', __name__) +product_service = ProductService() + +image_root_url = os.getenv('IMAGE_ROOT_URL') +missing_image_file = "product_image_coming_soon.png" + + + +def should_fully_qualify_image_urls() -> bool: + param = request.args.get("fullyQualifyimageUrls", "1") + return param.lower() in ["1", "true"] + +def fully_qualify_image_url(image: str, category_name: str) -> str: + if image and image != missing_image_file: + return f"{image_root_url}{category_name}/{image}" + else: + return f"{image_root_url}{missing_image_file}" + + +def set_fully_qualified_category_image_url(category: Dict[str, Any]): + if should_fully_qualify_image_urls(): + category["image"] = fully_qualify_image_url(category.get("image"), category.get("name")) + elif not category.get("image") or category["image"] == missing_image_file: + category["image"] = f"{image_root_url}{missing_image_file}" + + +def set_fully_qualified_product_image_url(product: Dict[str, Any]): + if should_fully_qualify_image_urls(): + product["image"] = fully_qualify_image_url(product.get("image"), product.get("category")) + elif not product.get("image") or product["image"] == missing_image_file: + product["image"] = f"{image_root_url}{missing_image_file}" + + +def set_fully_qualified_category_image_urls(categories: List[Dict[str, Any]]): + for category in categories: + set_fully_qualified_category_image_url(category) + + +def set_fully_qualified_product_image_urls(products: List[Dict[str, Any]]): + for product in products: + set_fully_qualified_product_image_url(product) + +def custom_serializer(obj): + if isinstance(obj, Decimal): + return float(obj) + raise TypeError("Type not serializable") + +def validate_product(product): + if not product['name']: + raise HTTPException('Product name is required', 422) + + if product['price'] < 0: + raise HTTPException('Product price cannot be a negative value', 422) + + if product['current_stock'] < 0: + raise HTTPException('Product current stock cannot be a negative value', 422) + + if product['category']: + category = product_service.get_category_by_name(product['category']) + if not category: + raise HTTPException('Invalid product category; does not exist', 422) + + +@app.route('/') +def index(): + app.logger.info('Processing default request') + return jsonify("Welcome to the Products Web Service"),200 + +@app.route('/products/all', methods=['GET']) +def get_all_products(): + app.logger.info('Processing get all products request') + products = product_service.get_all_products() + set_fully_qualified_product_image_urls(products) + return Response(json.dumps(products, default=custom_serializer), mimetype='application/json'), 200 + +@app.route('/products/id/', methods=['GET', 'PUT', 'DELETE']) +def get_products_by_id(product_ids): + if request.method == 'GET': + app.logger.info('Processing get products by ID request') + product_ids = product_ids.split(",") + if len(product_ids) > product_service.MAX_BATCH_GET_ITEM: + return jsonify({"error": f"Maximum number of product IDs per request is {product_service.MAX_BATCH_GET_ITEM}"}), HTTPStatus.UNPROCESSABLE_ENTITY + if len(product_ids) > 1: + products = product_service.get_products_by_ids(product_ids) + set_fully_qualified_product_image_urls(products) + return jsonify(products), 200 + + else: + product = product_service.get_product_by_id(product_ids[0]) + if not product: + abort(HTTPStatus.NOT_FOUND) + set_fully_qualified_product_image_url(product) + app.logger.info(f"retrieved product: {product}") + return jsonify(product), 200 + elif request.method == 'PUT': + app.logger.info('Processing update products by ID request') + product = request.get_json(force=True) + app.logger.info(f"Validating product: {product}") + try: + validate_product(product) + except HTTPException as e: + return jsonify({"error": e.description}), e.code + app.logger.info(f"retrieving existing product with id: {product_ids}") + existing_product = product_service.get_product_by_id(product_ids) + + if not existing_product: + return jsonify({"error": "Product does not exist"}), 404 + + try: + product_service.update_product(existing_product, product) + except Exception as e: + return jsonify({"error": e.description}), 422 + + set_fully_qualified_product_image_url(existing_product) + return jsonify(existing_product), 200 + elif request.method == 'DELETE': + app.logger.info('Processing delete product by ID request') + + product = product_service.get_product_by_id(product_ids) + + if not product: + return jsonify({"error": "Product does not exist"}), 404 + + try: + product_service.delete_product(product) + except Exception as e: + return jsonify({"error": e.description}), 422 + +@app.route('/products/featured', methods=['GET']) +def get_featured_products(): + app.logger.info('Processing get featured products request') + products = product_service.get_featured_products() + set_fully_qualified_product_image_urls(products) + return jsonify(products), 200 + +@app.route('/products/category/', methods=['GET']) +def get_products_by_category(category_name): + app.logger.info('Processing get products by category request') + products = product_service.get_product_by_category(category_name) + set_fully_qualified_product_image_urls(products) + return jsonify(products), 200 + +@app.route('/products', methods=['POST']) +def create_product(): + app.logger.info('Processing create product request') + product = request.get_json() + + try: + validate_product(product) + except HTTPException as e: + return jsonify({"error": e.description}), e.code + + try: + product_service.add_product(product) + except Exception as e: + return jsonify({"error": e.description}), 422 + + set_fully_qualified_product_image_url(product) + return jsonify(product), 201 + +@app.route('/products/id//inventory', methods=['PUT']) +def update_product_inventory(product_id): + app.logger.info('Processing update product inventory request') + inventory = request.get_json() + app.logger.info(f"UpdateInventory --> {inventory}") + + product = product_service.get_product_by_id(product_id) + + if not product: + return jsonify({"error": "Product does not exist"}), 404 + + try: + product_service.update_inventory_delta(product, inventory['stock_delta']) + except Exception as e: + return jsonify({"error": e.description}), 422 + + set_fully_qualified_category_image_url(product) + + return jsonify(product), 200 + + +@app.route('/categories/all', methods=['GET']) +def get_all_categories(): + app.logger.info('Processing get all categories request') + categories = product_service.get_all_categories() + set_fully_qualified_category_image_urls(categories) + return jsonify(categories), 200 + +@app.route('/categories/id/', methods=['GET']) +def get_categories_by_id(category_id): + app.logger.info('Processing get category by ID request') + + try: + category = product_service.get_category(category_id) + except Exception as e: + return jsonify({"error": e.description}), 422 + + set_fully_qualified_category_image_url(category) + + return jsonify(category), 200 \ No newline at end of file diff --git a/src/products/src/products-service/server.py b/src/products/src/products-service/server.py index a06ee4a0b..eaf15e1fe 100644 --- a/src/products/src/products-service/server.py +++ b/src/products/src/products-service/server.py @@ -3,6 +3,12 @@ from flask import Flask from flask_cors import CORS +import logging + + app = Flask(__name__) +handler = logging.StreamHandler() +handler.setLevel(logging.INFO) +app.logger.addHandler(handler) CORS(app) diff --git a/src/products/src/products-service/services.py b/src/products/src/products-service/services.py index d5ced041f..4548a1982 100644 --- a/src/products/src/products-service/services.py +++ b/src/products/src/products-service/services.py @@ -1,25 +1,27 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 -from flask import request from server import app import os -from boto3.dynamodb.conditions import Key from botocore.exceptions import ClientError -from boto3.dynamodb.types import TypeDeserializer +from boto3.dynamodb.types import TypeDeserializer,TypeSerializer from uuid import uuid4 -from aws import dynamo_client, ddb_table_products, ddb_table_categories +from dynamo_setup import dynamo_client, ddb_table_products, ddb_table_categories import ast from decimal import Decimal - class ProductService: + def __init__(self): self.dynamo_client = dynamo_client self.ddb_table_products = ddb_table_products self.ddb_table_categories = ddb_table_categories self.MAX_BATCH_GET_ITEM = 100 self.web_root_url = os.getenv("WEB_ROOT_URL") + self.serializer = TypeSerializer() + self.deserializer = TypeDeserializer() + + def set_product_url(self, product): if self.web_root_url: @@ -33,7 +35,7 @@ def set_category_url(self, category): def unmarshal_items(self, dynamodb_items): - deserializer = TypeDeserializer() + def is_number_string(s): return isinstance(s, str) and s.replace('.', '', 1).isdigit() @@ -57,7 +59,7 @@ def convert_boolean(v): else convert_decimal(float(v)) if is_number_string(v) else convert_boolean(v) if isinstance(v, bool) else convert_decimal(v) - for k, v in {k: deserializer.deserialize(v) for k, v in item.items()}.items()} + for k, v in {k: self.deserializer.deserialize(v) for k, v in item.items()}.items()} for item in dynamodb_items ] @@ -70,7 +72,6 @@ def marshal_item(self, data): } def unmarshal_items_categories(self, dynamodb_items): - deserializer = TypeDeserializer() def is_string_list(s): return isinstance(s, str) and s.startswith("[") and s.endswith("]") @@ -84,7 +85,7 @@ def is_boolean_string(s): else True if is_boolean_string(v) and v.lower() == "true" else False if is_boolean_string(v) and v.lower() == "false" else v - for k, v in {k: deserializer.deserialize(v) for k, v in item.items()}.items() + for k, v in {k: self.deserializer.deserialize(v) for k, v in item.items()}.items() } for item in dynamodb_items ] @@ -409,4 +410,5 @@ def delete_product(self, product): app.logger.error(e.response['Error']['Message']) return e - return None \ No newline at end of file + return None + diff --git a/src/products/src/products-service/services1.py b/src/products/src/products-service/services1.py new file mode 100644 index 000000000..8d90f7ea8 --- /dev/null +++ b/src/products/src/products-service/services1.py @@ -0,0 +1,339 @@ +import os +from server import app +from boto3.dynamodb.types import TypeSerializer, TypeDeserializer +from uuid import uuid4 +from dynamo_setup import ddb_table_categories, ddb_table_products, dynamo_resource + +class ProductService: + + dynamo_client = dynamo_resource.meta.client + ddb_table_products = ddb_table_products + ddb_table_categories = ddb_table_categories + MAX_BATCH_GET_ITEM = 100 + web_root_url = os.getenv("WEB_ROOT_URL") + serializer = TypeSerializer() + deserializer = TypeDeserializer() + ALLOWED_PRODUCT_KEYS = { + 'id', 'name', 'description', 'price', 'category', 'image', 'style', + 'gender_affinity', 'current_stock', 'promoted', 'featured', 'sk', 'aliases' + } + ALLOWED_CATEGORY_KEYS = {'id', 'name', 'image'} + + + @staticmethod + def validate_product(product): + invalid_keys = set(product.keys()) - ProductService.ALLOWED_PRODUCT_KEYS + if invalid_keys: + raise ValueError(f'Invalid keys: {invalid_keys}') + + @staticmethod + def validate_category(category): + invalid_keys = set(category.keys()) - ProductService.ALLOWED_CATEGORY_KEYS + if invalid_keys: + raise ValueError(f'Invalid keys: {invalid_keys}') + + @staticmethod + def unmarshal_items(dynamodb_items): + return dynamodb_items + + @staticmethod + def marshal_items(dynamodb_items): + return dynamodb_items + + @staticmethod + def get_product_template(): + return { + 'id': str(uuid4()), + 'aliases': [] + } + + @staticmethod + def update_product_template(product): + if 'aliases' not in product or not product['aliases']: + product['aliases'] = [] + if 'sk' not in product or product['sk'] is None: + product['sk'] = '' + return product + + @classmethod + def execute_and_log(cls, func, success_message, error_message, **kwargs): + try: + app.logger.info('Executing operation') + response = func(**kwargs) + app.logger.info(success_message) + return response + except Exception as e: + app.logger.error(f'Execution error, {error_message}: {str(e)}') + raise + + @classmethod + def set_product_url(cls, product): + if cls.web_root_url: + product["url"] = f"{cls.web_root_url}/#/product/{product['id']}" + + @classmethod + def set_category_url(cls, category): + if cls.web_root_url: + category["url"] = f"{cls.web_root_url}/#/category/{category['id']}" + + @classmethod + def get_product_by_id(cls, product_id): + product_id = str(product_id.lower()) + app.logger.info(f'Finding product with id: {product_id}, {cls.ddb_table_products}') + response = cls.execute_and_log( + cls.dynamo_client.get_item, + f'Retrieved product with id: {product_id}', + f'Error retrieving product with id: {product_id}', + TableName=cls.ddb_table_products, + Key={ + 'id': {'S': product_id} + } + ) + if 'Item' in response: + app.logger.info(f'Retrieved product: {response["Item"]}') + product = cls.unmarshal_items(response['Item']) + app.logger.info(f'Unmarshalled product: {product}') + cls.set_product_url(product) + cls.update_product_template(product) + app.logger.info(f"Found product: {product}, category: {product['category']}") + return product + else: + raise KeyError + + @classmethod + def get_products_by_ids(cls, product_ids): + if len(product_ids) > cls.MAX_BATCH_GET_ITEM: + raise Exception("Cannot query more than 100 items at a time") + + app.logger.info(f"Finding products with ids: {product_ids}, {cls.ddb_table_products}") + + request_items = { + cls.ddb_table_products: { + 'Keys': [{'id': product_id} for product_id in product_ids] + } + } + response = cls.execute_and_log( + cls.dynamo_client.batch_get_item, + f'Retrieved products with ids: {product_ids}', + f'Error retrieving products with ids: {product_ids}', + RequestItems=request_items + ) + products = [cls.unmarshal_items(item, cls.ALLOWED_PRODUCT_KEYS) for item in response.get('Responses', {}).get(cls.ddb_table_products, [])] + return products + + @classmethod + def get_category_by_id(cls, category_id): + category_id = category_id.lower() + app.logger.info(f"Finding category with id: {category_id}, {cls.ddb_table_categories}") + response = cls.execute_and_log( + cls.dynamo_client.get_item, + f'Retrieved category with id: {category_id}', + f'Error retrieving category with id: {category_id}', + TableName=cls.ddb_table_categories, + Key={'id': {'S': category_id}} + ) + if 'Item' in response: + category = cls.unmarshal_items(response['Item']) + cls.set_category_url(category) + app.logger.info(f"Found category: {category}") + return category + else: + raise KeyError + + @classmethod + def get_category_by_name(cls, category_name): + app.logger.info(f"Finding category with name: {category_name}, {cls.ddb_table_categories}") + response = cls.execute_and_log( + cls.dynamo_client.query, + f'Retrieved category with name: {category_name}', + f'Error retrieving category with name: {category_name}', + TableName=cls.ddb_table_categories, + IndexName='name-index', + ExpressionAttributeValues= { + ':category_name': {'S': category_name} + }, + KeyConditionExpression='#n = :category_name', + ExpressionAttributeNames={'#n': 'name'}, + ProjectionExpression='id, #n, image' + ) + if 'Items' in response: + category = cls.unmarshal_items(response['Items'][0]) + cls.set_category_url(category) + app.logger.info(f"Found category: {category}") + return category + else: + raise KeyError + + @classmethod + def get_product_by_category(cls, category): + app.logger.info(f"Finding products by category: {category}, {cls.ddb_table_products}") + + response = cls.execute_and_log( + cls.dynamo_client.query, + f'Retrieved products by category: {category}', + f'Error retrieving products by category: {category}', + TableName=cls.ddb_table_products, + IndexName='category-index', + ExpressionAttributeValues= { + ':category': {'S': category} + }, + KeyConditionExpression='category = :category', + ProjectionExpression='id, category, #n, image, #s, description, price, gender_affinity, current_stock, promoted', + ExpressionAttributeNames={ + '#n': 'name', + '#s': 'style' + } + ) + if 'Items' in response: + products = cls.unmarshal_items(response['Items']) + for product in products: + cls.set_product_url(product) + cls.update_product_template(product) + app.logger.info(f"Found products: {products}") + return products + + @classmethod + def get_featured_products(cls): + app.logger.info(f"Finding featured products, {cls.ddb_table_products} | featured=true") + + + response = cls.execute_and_log( + cls.dynamo_client.query, + f'Retrieved featured products', + f'Error retrieving featured products', + TableName=cls.ddb_table_products, + IndexName='featured-index', + ExpressionAttributeValues= { + ':featured': {'S': 'true'} + }, + KeyConditionExpression='featured = :featured', + ProjectionExpression='id, category, #n, image, #s, description, price, gender_affinity, current_stock, promoted', + ExpressionAttributeNames={ + '#n': 'name', + '#s': 'style' + } + ) + if 'Items' in response: + products = cls.unmarshal_items(response['Items']) + for product in products: + cls.set_product_url(product) + cls.update_product_template(product) + product['featured'] = 'true' + app.logger.info(f"Found featured product: {product}") + return products + + @classmethod + def get_all_categories(cls): + app.logger.info(f"Finding all categories, {cls.ddb_table_categories}") + + response = cls.execute_and_log( + cls.dynamo_client.scan, + f'Retrieved all categories', + f'Error retrieving all categories', + TableName=cls.ddb_table_categories + ) + if 'Items' in response: + app.logger.info(f"Found {len(response['Items'])} categories") + categories = cls.unmarshal_items(response['Items']) + for category in categories: + cls.set_category_url(category) + return categories + + @classmethod + def get_all_products(cls): + app.logger.info(f"Finding all products, {cls.ddb_table_products}") + + response = cls.execute_and_log( + cls.dynamo_client.scan, + f'Retrieved all products', + f'Error retrieving all products', + TableName=cls.ddb_table_products + ) + if 'Items' in response: + app.logger.info(f"Found {len(response['Items'])} products") + products = cls.unmarshal_items(response['Items']) + for product in products: + cls.set_product_url(product) + cls.update_product_template(product) + if product['id'] == '8bffb5fb-624f-48a8-a99f-b8e9c64bbe29': + tshoot = product + app.logger.info(f"Found products: {tshoot}") + app.logger.info(f"Found products: {products[0:2]}") + return products + + @classmethod + def update_product(cls, original_product, updated_product): + app.logger.info(f"Updating product: {original_product} to {updated_product}") + updated_product['id'] = original_product['id'] + cls.set_product_url(updated_product) + prep_for_marshal = [] + prep_for_marshal.append(updated_product) + updated_product = cls.marshal_item(prep_for_marshal) + + + app.logger.info(f"Updating product: {original_product} to {updated_product}") + cls.execute_and_log( + cls.dynamo_client.put_item, + f'Updated product: {updated_product}', + f'Error updating product: {updated_product}', + TableName=cls.ddb_table_products, + Item=updated_product + ) + + @classmethod + def update_inventory_delta(cls, product, stock_delta): + app.logger.info(f"Updating inventory delta for product: {product['name']}") + + if product['current_stock'] + stock_delta < 0: + stock_delta = -product['current_stock'] + + params = { + 'TableName': ddb_table_products, + 'Key': { + 'id': {'S': product['id']}, + 'category': {'S': product['category']} + }, + 'ExpressionAttributeValues': { + ':stock_delta': {'N': str(stock_delta)}, + ':current_stock': {'N': str(product['current_stock'])} + }, + 'UpdateExpression': 'SET current_stock = current_stock + :stock_delta', + 'ConditionExpression': 'current_stock + :stock_delta >= 0', + 'ReturnValues': 'UPDATED_NEW' + } + + cls.execute_and_log( + cls.dynamo_client.update_item, + f'Updated product: {product}', + f'Error updating product: {product}', + **params) + + product['current_stock'] += stock_delta + + @classmethod + def add_product(cls, product): + product_temp = cls.get_product_template() + app.logger.info(f"Adding product: {product}") + product.update(product_temp) + cls.update_product_template(product) + cls.set_product_url(product) + marshalled_product = cls.marshal_item([product]) + cls.execute_and_log( + cls.dynamo_client.put_item, + f'Added product: {marshalled_product}', + f'Error adding product: {marshalled_product}', + TableName=cls.ddb_table_products, + Item=marshalled_product + ) + + @classmethod + def delete_product(cls, product): + app.logger.info(f"Deleting product: {product['name']}") + + cls.execute_and_log( + cls.dynamo_client.delete_item, + f'Deleted product: {product}', + f'Error deleting product: {product}', + TableName=cls.ddb_table_products, + Key={'id': {'S': product['id']}, 'category': {'S': product['category']}} + ) \ No newline at end of file diff --git a/src/testing/testhelpers.egg-info/PKG-INFO b/src/testing/testhelpers.egg-info/PKG-INFO new file mode 100644 index 000000000..cb3171d9d --- /dev/null +++ b/src/testing/testhelpers.egg-info/PKG-INFO @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: testhelpers +Version: 1.0 diff --git a/src/testing/testhelpers.egg-info/SOURCES.txt b/src/testing/testhelpers.egg-info/SOURCES.txt new file mode 100644 index 000000000..cf0395107 --- /dev/null +++ b/src/testing/testhelpers.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +README.md +setup.py +testhelpers/__init__.py +testhelpers/integ.py +testhelpers.egg-info/PKG-INFO +testhelpers.egg-info/SOURCES.txt +testhelpers.egg-info/dependency_links.txt +testhelpers.egg-info/requires.txt +testhelpers.egg-info/top_level.txt \ No newline at end of file diff --git a/src/testing/testhelpers.egg-info/dependency_links.txt b/src/testing/testhelpers.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/testing/testhelpers.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/testing/testhelpers.egg-info/requires.txt b/src/testing/testhelpers.egg-info/requires.txt new file mode 100644 index 000000000..8a8fb82c4 --- /dev/null +++ b/src/testing/testhelpers.egg-info/requires.txt @@ -0,0 +1,3 @@ +requests +jsonschema +assertpy diff --git a/src/testing/testhelpers.egg-info/top_level.txt b/src/testing/testhelpers.egg-info/top_level.txt new file mode 100644 index 000000000..6e7d065dd --- /dev/null +++ b/src/testing/testhelpers.egg-info/top_level.txt @@ -0,0 +1 @@ +testhelpers