├── .github
└── workflows
│ └── run-unit-tests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CODE_OF_CONDUCT.md
├── CommunityFridgeMapApi
├── .gitignore
├── README.md
├── __init__.py
├── api_contract.yaml
├── dependencies
│ ├── __init__.py
│ └── python
│ │ ├── __init__.py
│ │ ├── db.py
│ │ ├── requirements.txt
│ │ └── s3_service.py
├── events
│ ├── event.json
│ ├── local-contact-email.json
│ ├── local-event-get-fridge.json
│ ├── local-event-get-fridges-with-tag.json
│ ├── local-event-get-fridges.json
│ ├── local-fridge-report-event.json
│ └── local-post-fridge-event.json
├── functions
│ ├── __init__.py
│ ├── dev
│ │ ├── README.md
│ │ ├── __init__.py
│ │ ├── hello_world
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ └── requirements.txt
│ │ └── load_fridge_data
│ │ │ ├── __init__.py
│ │ │ ├── app.py
│ │ │ └── requirements.txt
│ ├── email_service
│ │ └── v1
│ │ │ ├── __init__.py
│ │ │ └── app.py
│ ├── fridge_reports
│ │ ├── __init__.py
│ │ ├── app.py
│ │ └── get_latest_fridge_reports
│ │ │ ├── __init__.py
│ │ │ └── app.py
│ ├── fridges
│ │ └── v1
│ │ │ ├── __init__.py
│ │ │ └── app.py
│ └── image
│ │ └── v1
│ │ ├── __init__.py
│ │ └── app.py
├── requirements.txt
├── template.yaml
└── tests
│ ├── __init__.py
│ ├── assert_resposne.py
│ ├── conftest.py
│ ├── integration
│ ├── __init__.py
│ └── test_api_gateway.py
│ ├── requirements.txt
│ ├── s3_service_stub.py
│ └── unit
│ ├── __init__.py
│ ├── test_db_item.py
│ ├── test_db_response.py
│ ├── test_email_service.py
│ ├── test_fridge.py
│ ├── test_fridge_handler.py
│ ├── test_fridge_report.py
│ ├── test_fridge_report_handler.py
│ ├── test_get_latest_fridge_report.py
│ ├── test_hello_world.py
│ ├── test_image.py
│ ├── test_layer.py
│ ├── test_post_fridge_handler.py
│ ├── test_s3_service.py
│ └── test_tag.py
├── LICENSE
├── README.md
├── docker-compose.yml
├── schema
├── fridge.json
├── fridge_history.json
├── fridge_report.json
└── tag.json
└── scripts
└── create_local_dynamodb_tables.py
/.github/workflows/run-unit-tests.yml:
--------------------------------------------------------------------------------
1 | name: Run Unit Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.10"]
11 | steps:
12 | - uses: actions/checkout@v3
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v4
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install dependencies
18 | run: |
19 | REQUIREMENTS="CommunityFridgeMapApi/tests/requirements.txt"
20 | if [ -f $REQUIREMENTS ]; then pip install -r $REQUIREMENTS --user; fi
21 | - name: Run tests
22 | run: |
23 | cd CommunityFridgeMapApi/
24 | python -m pytest tests/unit -v
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # AWS SAM
2 | /volume
3 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v2.3.0
4 | hooks:
5 | - id: end-of-file-fixer
6 | - id: trailing-whitespace
7 | - repo: https://github.com/psf/black
8 | rev: 22.3.0
9 | hooks:
10 | - id: black
11 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct: Fridge Finder
2 |
3 | Like the technical community as a whole, the Fridge Map team is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people. Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance.
4 |
5 | This isn't an exhaustive list of things that you can't do. Rather, take it in the spirit in which it's intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. This code of conduct applies to all spaces managed by the Fridge Map project. This includes the Discord Server, the Trello Board, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them.
6 |
7 | If you believe someone is violating the code of conduct, we ask that you report it by emailing .
8 |
9 | - **Be friendly and patient.**
10 |
11 | - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
12 |
13 | - **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
14 |
15 | - **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It's important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Fridge Map community should be respectful when dealing with other members as well as with people outside the Fridge Map community.
16 |
17 | - **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do
18 | not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
19 |
20 | - Violent threats or language directed against another person.
21 | - Discriminatory jokes and language.
22 | - Posting sexually explicit or violent material.
23 | - Posting (or threatening to post) other people's personally identifying information ("doxing").
24 | - Personal insults, especially those using racist or sexist terms.
25 | - Unwelcome sexual attention.
26 | - Advocating for, or encouraging, any of the above behavior.
27 | - Repeated harassment of others. In general, if someone asks you to stop, then stop.
28 |
29 | - **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Fridge Map is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we're different. The strength of Fridge Map comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn't mean that they're wrong. Don't forget that it is human to err and blaming each other doesn't get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
30 |
31 | Original text courtesy of the [Django Project](https://www.djangoproject.com/conduct/)
32 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
3 |
4 | ### Linux ###
5 | *~
6 |
7 | # temporary files which can be created if a process still has a handle open of a deleted file
8 | .fuse_hidden*
9 |
10 | # KDE directory preferences
11 | .directory
12 |
13 | # Linux trash folder which might appear on any partition or disk
14 | .Trash-*
15 |
16 | # .nfs files are created when an open file is removed but is still being accessed
17 | .nfs*
18 |
19 | ### OSX ###
20 | *.DS_Store
21 | .AppleDouble
22 | .LSOverride
23 |
24 | # Icon must end with two \r
25 | Icon
26 |
27 | # Thumbnails
28 | ._*
29 |
30 | # Files that might appear in the root of a volume
31 | .DocumentRevisions-V100
32 | .fseventsd
33 | .Spotlight-V100
34 | .TemporaryItems
35 | .Trashes
36 | .VolumeIcon.icns
37 | .com.apple.timemachine.donotpresent
38 |
39 | # Directories potentially created on remote AFP share
40 | .AppleDB
41 | .AppleDesktop
42 | Network Trash Folder
43 | Temporary Items
44 | .apdisk
45 |
46 | ### PyCharm ###
47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
49 |
50 | # User-specific stuff:
51 | .idea/**/workspace.xml
52 | .idea/**/tasks.xml
53 | .idea/dictionaries
54 |
55 | # Sensitive or high-churn files:
56 | .idea/**/dataSources/
57 | .idea/**/dataSources.ids
58 | .idea/**/dataSources.xml
59 | .idea/**/dataSources.local.xml
60 | .idea/**/sqlDataSources.xml
61 | .idea/**/dynamic.xml
62 | .idea/**/uiDesigner.xml
63 |
64 | # Gradle:
65 | .idea/**/gradle.xml
66 | .idea/**/libraries
67 |
68 | # CMake
69 | cmake-build-debug/
70 |
71 | # Mongo Explorer plugin:
72 | .idea/**/mongoSettings.xml
73 |
74 | ## File-based project format:
75 | *.iws
76 |
77 | ## Plugin-specific files:
78 |
79 | # IntelliJ
80 | /out/
81 |
82 | # mpeltonen/sbt-idea plugin
83 | .idea_modules/
84 |
85 | # JIRA plugin
86 | atlassian-ide-plugin.xml
87 |
88 | # Cursive Clojure plugin
89 | .idea/replstate.xml
90 |
91 | # Ruby plugin and RubyMine
92 | /.rakeTasks
93 |
94 | # Crashlytics plugin (for Android Studio and IntelliJ)
95 | com_crashlytics_export_strings.xml
96 | crashlytics.properties
97 | crashlytics-build.properties
98 | fabric.properties
99 |
100 | ### PyCharm Patch ###
101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
102 |
103 | # *.iml
104 | # modules.xml
105 | # .idea/misc.xml
106 | # *.ipr
107 |
108 | # Sonarlint plugin
109 | .idea/sonarlint
110 |
111 | ### Python ###
112 | # Byte-compiled / optimized / DLL files
113 | __pycache__/
114 | *.py[cod]
115 | *$py.class
116 |
117 | # C extensions
118 | *.so
119 |
120 | # Distribution / packaging
121 | .Python
122 | build/
123 | develop-eggs/
124 | dist/
125 | downloads/
126 | eggs/
127 | .eggs/
128 | lib/
129 | lib64/
130 | parts/
131 | sdist/
132 | var/
133 | wheels/
134 | *.egg-info/
135 | .installed.cfg
136 | *.egg
137 |
138 | # PyInstaller
139 | # Usually these files are written by a python script from a template
140 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
141 | *.manifest
142 | *.spec
143 |
144 | # Installer logs
145 | pip-log.txt
146 | pip-delete-this-directory.txt
147 |
148 | # Unit test / coverage reports
149 | htmlcov/
150 | .tox/
151 | .coverage
152 | .coverage.*
153 | .cache
154 | .pytest_cache/
155 | nosetests.xml
156 | coverage.xml
157 | *.cover
158 | .hypothesis/
159 |
160 | # Translations
161 | *.mo
162 | *.pot
163 |
164 | # Flask stuff:
165 | instance/
166 | .webassets-cache
167 |
168 | # Scrapy stuff:
169 | .scrapy
170 |
171 | # Sphinx documentation
172 | docs/_build/
173 |
174 | # PyBuilder
175 | target/
176 |
177 | # Jupyter Notebook
178 | .ipynb_checkpoints
179 |
180 | # pyenv
181 | .python-version
182 |
183 | # celery beat schedule file
184 | celerybeat-schedule.*
185 |
186 | # SageMath parsed files
187 | *.sage.py
188 |
189 | # Environments
190 | .env
191 | .venv
192 | env/
193 | venv/
194 | ENV/
195 | env.bak/
196 | venv.bak/
197 |
198 | # Spyder project settings
199 | .spyderproject
200 | .spyproject
201 |
202 | # Rope project settings
203 | .ropeproject
204 |
205 | # mkdocs documentation
206 | /site
207 |
208 | # mypy
209 | .mypy_cache/
210 |
211 | ### VisualStudioCode ###
212 | .vscode/*
213 | !.vscode/settings.json
214 | !.vscode/tasks.json
215 | !.vscode/launch.json
216 | !.vscode/extensions.json
217 | .history
218 |
219 | ### Windows ###
220 | # Windows thumbnail cache files
221 | Thumbs.db
222 | ehthumbs.db
223 | ehthumbs_vista.db
224 |
225 | # Folder config file
226 | Desktop.ini
227 |
228 | # Recycle Bin used on file shares
229 | $RECYCLE.BIN/
230 |
231 | # Windows Installer files
232 | *.cab
233 | *.msi
234 | *.msm
235 | *.msp
236 |
237 | # Windows shortcuts
238 | *.lnk
239 |
240 | # Build folder
241 |
242 | */build/*
243 |
244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
245 |
246 | # AWS Sam
247 | *.aws-sam
248 | local-event.json
249 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/README.md:
--------------------------------------------------------------------------------
1 | # CommunityFridgeMapApi
2 |
3 | This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders.
4 |
5 | - hello_world - Code for the application's Lambda function.
6 | - events - Invocation events that you can use to invoke the function.
7 | - tests - Unit tests for the application code.
8 | - template.yaml - A template that defines the application's AWS resources.
9 |
10 | The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code.
11 |
12 | If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit.
13 | The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started.
14 |
15 | * [CLion](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
16 | * [GoLand](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
17 | * [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
18 | * [WebStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
19 | * [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
20 | * [PhpStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
21 | * [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
22 | * [RubyMine](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
23 | * [DataGrip](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
24 | * [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html)
25 | * [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html)
26 |
27 | ## Deploy the sample application
28 |
29 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API.
30 |
31 | To use the SAM CLI, you need the following tools.
32 |
33 | * SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
34 | * [Python 3 installed](https://www.python.org/downloads/)
35 | * Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community)
36 |
37 | To build and deploy your application for the first time, run the following in your shell:
38 |
39 | ```bash
40 | sam build --use-container
41 | sam deploy --guided
42 | ```
43 |
44 | The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts:
45 |
46 | * **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name.
47 | * **AWS Region**: The AWS region you want to deploy your app to.
48 | * **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes.
49 | * **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command.
50 | * **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application.
51 |
52 | You can find your API Gateway Endpoint URL in the output values displayed after deployment.
53 |
54 | ## Use the SAM CLI to build and test locally
55 |
56 | Build your application with the `sam build --use-container` command.
57 |
58 | ```bash
59 | CommunityFridgeMapApi$ sam build --use-container
60 | ```
61 |
62 | The SAM CLI installs dependencies defined in `hello_world/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder.
63 |
64 | Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project.
65 |
66 | Run functions locally and invoke them with the `sam local invoke` command.
67 |
68 | ```bash
69 | CommunityFridgeMapApi$ sam local invoke HelloWorldFunction --event events/event.json
70 | ```
71 |
72 | The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000.
73 |
74 | ```bash
75 | CommunityFridgeMapApi$ sam local start-api
76 | CommunityFridgeMapApi$ curl http://localhost:3000/
77 | ```
78 |
79 | The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path.
80 |
81 | ```yaml
82 | Events:
83 | HelloWorld:
84 | Type: Api
85 | Properties:
86 | Path: /hello
87 | Method: get
88 | ```
89 |
90 | ## Add a resource to your application
91 | The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types.
92 |
93 | ## Fetch, tail, and filter Lambda function logs
94 |
95 | To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug.
96 |
97 | `NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM.
98 |
99 | ```bash
100 | CommunityFridgeMapApi$ sam logs -n HelloWorldFunction --stack-name CommunityFridgeMapApi --tail
101 | ```
102 |
103 | You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html).
104 |
105 | ## Tests
106 |
107 | Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests.
108 |
109 | ```bash
110 | CommunityFridgeMapApi$ pip install -r tests/requirements.txt --user
111 | # unit test
112 | CommunityFridgeMapApi$ python -m pytest tests/unit -v
113 | # integration test, requiring deploying the stack first.
114 | # Create the env variable AWS_SAM_STACK_NAME with the name of the stack we are testing
115 | CommunityFridgeMapApi$ AWS_SAM_STACK_NAME= python -m pytest tests/integration -v
116 | ```
117 |
118 | ## Cleanup
119 |
120 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following:
121 |
122 | ```bash
123 | aws cloudformation delete-stack --stack-name CommunityFridgeMapApi
124 | ```
125 |
126 | ## Resources
127 |
128 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts.
129 |
130 | Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/)
131 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/api_contract.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | description: Community Fridge Map API
4 | version: 1.0.0
5 | title: Community Fridge Map API
6 | contact:
7 | email: fridgefinderapp@gmail.com
8 | license:
9 | name: Apache 2.0
10 | url: http://www.apache.org/licenses/LICENSE-2.0.html
11 |
12 | tags:
13 | - name: fridge
14 | description: Fridge root record
15 | - name: report
16 | description: Fridge status reports
17 | - name: contact
18 | description: Website contact form data
19 | - name: tag
20 | description: Hash tag for filtering and searching
21 |
22 | paths:
23 | /v1/fridges:
24 | get:
25 | summary: Read all Fridge objects.
26 | tags:
27 | - fridge
28 | responses:
29 | "200":
30 | description: Array of Fridge objects as JSON
31 | content:
32 | application/json:
33 | schema:
34 | type: array
35 | items:
36 | $ref: "#/components/schemas/Fridge"
37 | "400":
38 | description: Request could not be understood due to incorrect syntax.
39 | "401":
40 | description: Request requires user authentication.
41 | "404":
42 | description: Fridge table not found in database.
43 | "500":
44 | description: Unexpected error prevented server from fulfilling request.
45 |
46 | post:
47 | summary: Create a Fridge object.
48 | tags:
49 | - fridge
50 | requestBody:
51 | required: true
52 | content:
53 | application/json:
54 | schema:
55 | $ref: "#/components/schemas/Fridge"
56 | responses:
57 | "201":
58 | description: Fridge object was created successfully.
59 | content:
60 | application/json:
61 | schema:
62 | type: object
63 | properties:
64 | id:
65 | type: string
66 | pattern: "^[a-z0-9]{2,}$"
67 | description: ID of the Fridge object (fridgeId).
68 | example: greenpointfridge
69 | "400":
70 | description: Request could not be understood due to incorrect syntax.
71 | "401":
72 | description: Request requires user authentication.
73 | "404":
74 | description: Fridge table not found in database.
75 | "409":
76 | description: There is an existing Fridge object with the same fridge name.
77 | "500":
78 | description: Unexpected error prevented server from fulfilling request.
79 |
80 | /v1/fridges/{id}:
81 | parameters:
82 | - in: path
83 | name: id
84 | schema:
85 | type: string
86 | pattern: "^[a-z0-9]{2,}$"
87 | required: true
88 | description: ID of the Fridge object (fridgeId).
89 | example: greenpointfridge
90 |
91 | get:
92 | summary: Read a Fridge object.
93 | tags:
94 | - fridge
95 | responses:
96 | "200":
97 | description: A Fridge object as JSON
98 | content:
99 | application/json:
100 | schema:
101 | $ref: "#/components/schemas/Fridge"
102 | "400":
103 | description: Invalid Fridge ID.
104 | "401":
105 | description: Request requires user authentication.
106 | "404":
107 | description: Fridge object not found.
108 | "500":
109 | description: Unexpected error prevented server from fulfilling request.
110 |
111 | put:
112 | summary: Update a Fridge object.
113 | tags:
114 | - fridge
115 | requestBody:
116 | required: true
117 | content:
118 | application/json:
119 | schema:
120 | type: object
121 | properties:
122 | fridge:
123 | $ref: "#/components/schemas/Fridge"
124 | responses:
125 | "201":
126 | description: Fridge object was updated successfully.
127 | content:
128 | application/json:
129 | schema:
130 | type: object
131 | properties:
132 | id:
133 | type: string
134 | pattern: "^[a-z0-9]{2,}$"
135 | description: ID of the Fridge object (fridgeId).
136 | example: greenpointfridge
137 | "400":
138 | description: Invalid Fridge ID.
139 | "401":
140 | description: Request requires user authentication.
141 | "404":
142 | description: Fridge object not found.
143 | "500":
144 | description: Unexpected error prevented server from fulfilling request.
145 |
146 | delete:
147 | summary: Delete a Fridge object.
148 | tags:
149 | - fridge
150 | responses:
151 | "204":
152 | description: The Fridge object was deleted successfully.
153 | "400":
154 | description: Invalid Fridge ID.
155 | "401":
156 | description: Request requires user authentication.
157 | "404":
158 | description: Fridge object not found.
159 | "500":
160 | description: Unexpected error prevented server from fulfilling request.
161 |
162 | /v1/fridges/{id}/reports:
163 | parameters:
164 | - in: path
165 | name: id
166 | schema:
167 | type: string
168 | pattern: "^[a-z0-9]{2,}$"
169 | required: true
170 | description: ID of the Fridge object (fridgeId).
171 | example: greenpointfridge
172 |
173 | get:
174 | summary: Read all Report objects for the specified fridge.
175 | tags:
176 | - report
177 | responses:
178 | "200":
179 | description: Array of Report objects
180 | content:
181 | application/json:
182 | schema:
183 | type: array
184 | items:
185 | $ref: "#/components/schemas/Report"
186 | "400":
187 | description: Invalid Fridge ID.
188 | "401":
189 | description: Request requires user authentication.
190 | "404":
191 | description: Report objects not found for specified Fridge ID.
192 | "500":
193 | description: Unexpected error prevented server from fulfilling request.
194 |
195 | post:
196 | summary: Create a new Report object.
197 | tags:
198 | - report
199 | requestBody:
200 | required: true
201 | content:
202 | application/json:
203 | schema:
204 | $ref: "#/components/schemas/Report"
205 | responses:
206 | "201":
207 | description: Report object was created successfully.
208 | content:
209 | application/json:
210 | schema:
211 | type: object
212 | properties:
213 | timestamp:
214 | type: string
215 | format: date-time
216 | example: "2022-03-29T18:10:38.547Z"
217 | fridgeId:
218 | type: string
219 | pattern: "^[a-z0-9]{2,}$"
220 | example: greenpointfridge
221 | "400":
222 | description: Invalid Fridge ID.
223 | "401":
224 | description: Request requires user authentication.
225 | "404":
226 | description: Report objects not found for specified Fridge ID.
227 | "500":
228 | description: Unexpected error prevented server from fulfilling request.
229 |
230 | /v1/photo:
231 | post:
232 | summary: Create a new image file in the S3 bucket.
233 | tags:
234 | - fridge
235 | - report
236 | requestBody:
237 | required: true
238 | content:
239 | image/webp:
240 | schema:
241 | description: Blob data of the image.
242 | type: string
243 | format: binary
244 |
245 | responses:
246 | "201":
247 | description: Image was created successfully.
248 | content:
249 | application/json:
250 | schema:
251 | type: object
252 | properties:
253 | photoUrl:
254 | description: Fully qualified URL path to the image.
255 | format: uri
256 | example: https://s3.amazonaws.com/bucket/path/fridge.webp
257 | "400":
258 | description: Request could not be understood due to incorrect syntax.
259 | "401":
260 | description: Request requires user authentication.
261 | "500":
262 | description: Unexpected error prevented server from fulfilling request.
263 |
264 | /v1/contact:
265 | post:
266 | summary: Store website user message
267 | tags:
268 | - contact
269 | requestBody:
270 | required: true
271 | content:
272 | application/json:
273 | schema:
274 | $ref: '#/components/schemas/Contact'
275 | responses:
276 | '201':
277 | description: Message was successfully received.
278 | '400':
279 | description: Request could not be understood due to incorrect syntax.
280 | '500':
281 | description: Unexpected error prevented server from fulfilling request.
282 |
283 | components:
284 | schemas:
285 | Fridge:
286 | type: object
287 |
288 | required:
289 | - id
290 | - name
291 | - location
292 |
293 | properties:
294 | id:
295 | description: The database key for this fridge record.
296 | type: string
297 | pattern: '^[a-z0-9]{4,60}$'
298 | minLength: 4
299 | maxLength: 60
300 | readOnly: true
301 | nullable: false
302 | example: greenpointfridge
303 |
304 | name:
305 | description: A descriptive title for this fridge location.
306 | type: string
307 | minLength: 4
308 | maxLength: 60
309 | example: Greenpoint Fridge
310 |
311 | location:
312 | $ref: '#/components/schemas/Location'
313 |
314 | tags:
315 | description: Arbitrary words to associate with this fridge.
316 | type: array
317 | items:
318 | type: string
319 | maxLength: 140
320 | nullable: true
321 | example: [harlem, halal, kashrut]
322 |
323 | maintainer:
324 | $ref: '#/components/schemas/Maintainer'
325 | nullable: true
326 |
327 | photoUrl:
328 | description: Fully qualified URL path to a picture of the fridge.
329 | type: string
330 | format: uri
331 | nullable: true
332 | example: https://s3.amazonaws.com/bucket/path/fridge.webp
333 |
334 | notes:
335 | description: Notes about the fridge.
336 | type: string
337 | minLength: 1
338 | maxLength: 300
339 | nullable: true
340 | example: Next to Lot Radio.
341 |
342 | verified:
343 | description: Signifies that the fridge data has been verified by the fridge maintainer.
344 | type: boolean
345 | default: false
346 | readOnly: true
347 | example: false
348 |
349 | Location:
350 | type: object
351 |
352 | required:
353 | - street
354 | - city
355 | - state
356 | - zip
357 | - geoLat
358 | - geoLng
359 |
360 | properties:
361 | name:
362 | type: string
363 | maxLength: 70
364 | example: Ralph and Nash Deli
365 |
366 | street:
367 | type: string
368 | maxLength: 55
369 | example: 352 West 116th Street
370 |
371 | city:
372 | type: string
373 | maxLength: 35
374 | example: New York
375 |
376 | state:
377 | type: string
378 | pattern: '^[A-Z]{2}$'
379 | minLength: 2
380 | maxLength: 2
381 | example: NY
382 |
383 | zip:
384 | type: string
385 | pattern: '(^\d{5}$)|(^\d{5}-\d{4}$)'
386 | example: 10026
387 |
388 | geoLat:
389 | description: Geographic latitude.
390 | type: number
391 | readOnly: true
392 | example: 40.8049571
393 |
394 | geoLng:
395 | description: Geographic longitude.
396 | type: number
397 | readOnly: true
398 | example: -73.9570766
399 |
400 | Maintainer:
401 | type: object
402 |
403 | properties:
404 | name:
405 | description: Name of the contact person.
406 | type: string
407 | maxLength: 70
408 | example: Emily Ward
409 |
410 | email:
411 | type: string
412 | format: email
413 | example: eward@example.net
414 |
415 | organization:
416 | description: Name of the organization.
417 | type: string
418 | maxLength: 80
419 | nullable: true
420 | example: Food For People
421 |
422 | phone:
423 | type: string
424 | format: phone
425 | pattern: '^\(\d{3}\) \d{3}-\d{4}$'
426 | nullable: true
427 | example: (782) 654-4748
428 |
429 | website:
430 | type: string
431 | format: uri
432 | nullable: true
433 | example: http://gray.com/
434 |
435 | instagram:
436 | type: string
437 | format: uri
438 | nullable: true
439 | example: https://www.instagram.com/greenpointfridge/?hl=en
440 |
441 | Report:
442 | type: object
443 |
444 | required:
445 | - timestamp
446 | - condition
447 | - foodPercentage
448 |
449 | properties:
450 | timestamp:
451 | description: The ISO formatted date/time of this report.
452 | type: string
453 | format: date-time
454 | readOnly: true
455 | example: "2022-03-29T18:10:38.547Z"
456 |
457 | condition:
458 | description: The condition of the fridge. Fridge is ...
459 | type: string
460 | enum: [good, dirty, out of order, not at location]
461 |
462 | foodPercentage:
463 | description: The percentage of food in the fridge.
464 | type: integer
465 | enum: [0, 1, 2, 3]
466 |
467 | photoUrl:
468 | description: Fully qualified URL path to a picture of the food.
469 | type: string
470 | format: uri
471 | nullable: true
472 | example: https://s3.amazonaws.com/bucket/path/food.webp
473 |
474 | notes:
475 | description: Notes to be included with this report.
476 | type: string
477 | maxLength: 300
478 | nullable: true
479 | example: Filled with Mars bars and M&M candy.
480 |
481 | Contact:
482 | type: object
483 |
484 | required:
485 | - name
486 | - email
487 | - subject
488 | - message
489 |
490 | properties:
491 | name:
492 | description: Senders name.
493 | type: string
494 | minLength: 1
495 | maxLength: 70
496 | example: Emily Ward
497 |
498 | email:
499 | description: Senders email.
500 | type: string
501 | format: email
502 | example: eward@example.net
503 |
504 | subject:
505 | description: Title of the message.
506 | type: string
507 | minLength: 1
508 | maxLength: 70
509 | example: Questions About Press Opportunity
510 |
511 | message:
512 | description: Body of the message.
513 | type: string
514 | minLength: 1
515 | maxLength: 2048
516 | example: We are looking to write a piece about this website.
517 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/dependencies/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/dependencies/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/dependencies/python/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/dependencies/python/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/dependencies/python/db.py:
--------------------------------------------------------------------------------
1 | import os
2 | import boto3
3 | import time
4 | from botocore.exceptions import ClientError
5 | import logging
6 | from typing import Tuple
7 | import re
8 | import json
9 | from dataclasses import dataclass
10 | import datetime
11 |
12 | logger = logging.getLogger()
13 | logger.setLevel(logging.INFO)
14 |
15 |
16 | def get_ddb_connection(
17 | env: str = os.getenv("Environment", "")
18 | ) -> "botocore.client.DynamoDB":
19 | ddbclient = ""
20 | if env == "local":
21 | ddbclient = boto3.client("dynamodb", endpoint_url="http://localstack:4566/")
22 | else:
23 | ddbclient = boto3.client("dynamodb")
24 | return ddbclient
25 |
26 |
27 | def layer_test() -> str:
28 | return "hello world"
29 |
30 |
31 | @dataclass
32 | class Field_Validator:
33 | is_valid: str
34 | message: str
35 |
36 |
37 | class DB_Response:
38 | def __init__(
39 | self, success: bool, status_code: int, message: str, json_data: str = None):
40 | self.message = message
41 | self.status_code = status_code
42 | self.success = success
43 | self.json_data = json_data
44 |
45 | def is_successful(self) -> bool:
46 | return self.success
47 |
48 | def get_dict_form(self) -> dict:
49 | return {
50 | "messsage": self.message,
51 | "success": self.success,
52 | "status_code": self.status_code,
53 | "json_data": self.json_data,
54 | }
55 |
56 | def set_json_data(self, json_data: str):
57 | self.json_data = json_data
58 |
59 | def api_format(self) -> dict:
60 | if self.json_data:
61 | body = self.json_data
62 | else:
63 | body = json.dumps({"message": self.message})
64 | return {
65 | "statusCode": self.status_code,
66 | "headers": {
67 | "Content-Type": "application/json",
68 | "Access-Control-Allow-Origin": "*",
69 | },
70 | "body": (body),
71 | }
72 |
73 |
74 | class DB_Item:
75 |
76 | REQUIRED_FIELDS = []
77 | ITEM_TYPES = {}
78 | TABLE_NAME = ""
79 | FIELD_VALIDATION = {}
80 | STAGE = os.getenv("Stage", "")
81 |
82 | def __init__(self, db_client: "botocore.client.DynamoDB"):
83 | self.db_client = db_client
84 | pass
85 |
86 | def add_item(self, conditional_expression=None) -> DB_Response:
87 | """
88 | adds item to database
89 | Parameters:
90 | conditional_expression (str): conditional expression for boto3 function put_item
91 |
92 | Returns:
93 | db_response (DB_Response): returns a DB_Response
94 | """
95 | field_validation = self.validate_fields()
96 | if not field_validation.is_valid:
97 | return DB_Response(
98 | message=field_validation.message, status_code=400, success=False
99 | )
100 | item = self.format_dynamodb_item_v2()
101 | try:
102 | if conditional_expression:
103 | self.db_client.put_item(
104 | TableName=self.TABLE_NAME,
105 | Item=item,
106 | ConditionExpression=conditional_expression,
107 | )
108 | else:
109 | self.db_client.put_item(TableName=self.TABLE_NAME, Item=item)
110 | except self.db_client.exceptions.ConditionalCheckFailedException as e:
111 | return DB_Response(
112 | message=f"{self.TABLE_NAME} already exists, pick a different Name",
113 | status_code=409,
114 | success=False,
115 | )
116 | except self.db_client.exceptions.ResourceNotFoundException as e:
117 | message = f"Cannot do operations on a non-existent table: {self.TABLE_NAME}"
118 | logging.error(message)
119 | return DB_Response(message=message, status_code=500, success=False)
120 | except ClientError as e:
121 | logging.error(e)
122 | return DB_Response(
123 | message="Unexpected AWS service exception",
124 | status_code=500,
125 | success=False,
126 | )
127 | return DB_Response(
128 | message=f"{self.TABLE_NAME} was succesfully added",
129 | status_code=201,
130 | success=True,
131 | )
132 |
133 | def update_item(self):
134 | pass
135 |
136 | def get_item(self, primary_key):
137 | pass
138 |
139 | def delete_item(self):
140 | pass
141 |
142 | def get_all_items(self):
143 | """
144 | Gets all the Fridge Items
145 | NOTE: This function is probably fine for the fridges in NYC. But as more fridges
146 | are added to the database, this will be a bottleneck. Ideally we would be querying
147 | based on proximity. Here is an option for if we ever need to transition:
148 | https://hometechtime.com/how-to-build-a-dynamodb-geo-database-to-store-and-query-geospatial-data/
149 | """
150 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.scan
151 | return self.db_client.scan(TableName=self.TABLE_NAME)
152 |
153 | def has_required_fields(self) -> tuple:
154 | for field in self.REQUIRED_FIELDS:
155 | if getattr(self, field) is None:
156 | return (False, field)
157 | return (True, None)
158 |
159 | def get_class_field_value(self, key: str):
160 | """
161 | Gets the class field value based on a key.
162 | The parameter key can be the class field name or in the case of a dictionary can contain "/"
163 | "/" is used when a class field is a dictionary and the client wants to get the value of a child field
164 | Example: location: {geoLng: 23.4323}
165 | key = "location/geoLng"
166 | In order to get the value of geoLng the client would use the key "location/geoLng"
167 | Parameters:
168 | key (str): the name of the class field the client wants to obtain the value of
169 |
170 | Returns:
171 | object_dict (dict): the class field value
172 | """
173 | class_field_value = None
174 | if "/" in key:
175 | key_split = key.split("/")
176 | parent_key = key_split[0]
177 | class_field_value = getattr(self, parent_key)
178 | for i in range(1, len(key_split)):
179 | if class_field_value is None:
180 | break
181 | child_key = key_split[i]
182 | class_field_value = class_field_value.get(child_key, None)
183 | else:
184 | class_field_value = getattr(self, key)
185 | return class_field_value
186 |
187 | def format_dynamodb_item_v2(self):
188 | """
189 | Creates a dictionary in the syntax that dynamodb expects
190 | https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client.put_item
191 | """
192 | fridge_item = {}
193 | fridge_json = {}
194 | for key in self.FIELD_VALIDATION:
195 | if "/" in key:
196 | continue
197 | val = getattr(self, key)
198 | if val is not None:
199 | fridge_json[key] = val
200 | if isinstance(val, int) and not isinstance(val, bool):
201 | val = str(val)
202 | if isinstance(val, dict):
203 | val = json.dumps(val)
204 | elif isinstance(val, list):
205 | fridge_json[key] = val.copy()
206 | list_type = self.FIELD_VALIDATION[key]["list_type"]
207 | val = val.copy()
208 | for index, v in enumerate(val):
209 | val[index] = {list_type: v}
210 | fridge_item[key] = {self.FIELD_VALIDATION[key]["type"]: val}
211 | fridge_item["json_data"] = {"S": json.dumps(fridge_json)}
212 | return fridge_item
213 |
214 | @staticmethod
215 | def process_fields(object_dict: dict) -> dict:
216 | """
217 | Removes extra white spaces, trailing spaces, leading spaces from object_dict values
218 | If the length of the field is ZERO after removing the white spaces, sets the value to None
219 | Parameters:
220 | object_dict (dict): a dictinary
221 |
222 | Returns:
223 | object_dict (dict):
224 | """
225 | for key, value in object_dict.items():
226 | if isinstance(value, str):
227 | object_dict[key] = DB_Item.remove_extra_whitespace(value)
228 | if isinstance(value, dict):
229 | object_dict[key] = DB_Item.process_fields(value)
230 | if isinstance(value, list):
231 | for index, val in enumerate(value):
232 | if isinstance(val, str):
233 | value[index] = DB_Item.remove_extra_whitespace(val)
234 | return object_dict
235 |
236 | @staticmethod
237 | def remove_extra_whitespace(value: str):
238 | """
239 | Removes extra white spaces, trailing spaces, and leading spaces
240 | Example: Input: " hi there " Output: "hi there"
241 | Parameters:
242 | value (str): a string
243 | Returns:
244 | value (str): the value parameter without extra white spaces,
245 | trailing spaces, and leading spaces
246 | """
247 | value = re.sub(" +", " ", value).strip()
248 | if len(value) == 0:
249 | value = None
250 | return value
251 |
252 | def validate_fields(self) -> Field_Validator:
253 | """
254 | Validates that all the fields are valid.
255 | All fields are valid if they pass all the constraints set in FIELD_VALIDATION
256 | """
257 | for key, field_validation in self.FIELD_VALIDATION.items():
258 | class_field_value = self.get_class_field_value(key)
259 | is_not_none = class_field_value is not None
260 | required = field_validation.get("required", None)
261 | min_length = field_validation.get("min_length", None)
262 | max_length = field_validation.get("max_length", None)
263 | choices = field_validation.get("choices", None)
264 | if required:
265 | if class_field_value is None:
266 | return Field_Validator(
267 | is_valid=False, message=f"Missing Required Field: {key}"
268 | )
269 | if min_length and is_not_none:
270 | if len(str(class_field_value)) < min_length:
271 | return Field_Validator(
272 | is_valid=False,
273 | message=f"{key} character length must be >= {min_length}",
274 | )
275 | if max_length and is_not_none:
276 | if len(str(class_field_value)) > max_length:
277 | return Field_Validator(
278 | is_valid=False,
279 | message=f"{key} character length must be <= {max_length}",
280 | )
281 | if choices and is_not_none:
282 | if class_field_value not in choices:
283 | return Field_Validator(
284 | is_valid=False,
285 | message=f"{key} must to be one of: {str(choices)}",
286 | )
287 | return Field_Validator(
288 | is_valid=True, message="All Fields Were Successfully Validated"
289 | )
290 |
291 | def warm_lambda(self):
292 | key = {"id": {"S": "collectivefocusresourcehub"}}
293 | self.db_client.get_item(TableName=self.TABLE_NAME, Key=key)
294 |
295 |
296 | class Fridge(DB_Item):
297 | MIN_ID_LENGTH = 3
298 | MAX_ID_LENGTH = 100
299 | MAX_NAME_LENGTH = MAX_ID_LENGTH + 10
300 | FIELD_VALIDATION = {
301 | "id": {
302 | "required": True,
303 | "min_length": MIN_ID_LENGTH,
304 | "max_length": MAX_ID_LENGTH,
305 | "type": "S",
306 | },
307 | "name": {
308 | "required": True,
309 | "min_length": MIN_ID_LENGTH,
310 | "max_length": MAX_NAME_LENGTH,
311 | "type": "S",
312 | },
313 | "tags": {"required": False, "type": "L", "list_type": "S"},
314 | "location": {"required": True, "type": "S"},
315 | "location/name": {"required": False, "max_length": 256},
316 | "location/street": {"required": False, "max_length": 256},
317 | "location/city": {"required": False, "max_length": 256},
318 | "location/state": {"required": False, "max_length": 256},
319 | "location/zip": {"required": False, "max_length": 10},
320 | "location/geoLat": {"required": True, "max_length": 20},
321 | "location/geoLng": {"required": True, "max_length": 20},
322 | "location/country": {"required": False, "max_length": 256},
323 | "maintainer": {"required": False, "type": "S"},
324 | "maintainer/name": {"required": False, "max_length": 256},
325 | "maintainer/organization": {"required": False, "max_length": 256},
326 | "maintainer/phone": {"required": False, "min_length": 10, "max_length": 20},
327 | "maintainer/email": {"required": False, "max_length": 320},
328 | "maintainer/website": {"required": False, "max_length": 2048},
329 | "maintainer/instagram": {"required": False, "max_length": 64},
330 | "notes": {"required": False, "max_length": 700, "type": "S"},
331 | "food_accepts": {"required": False, "type": "L", "list_type": "S"},
332 | "food_restrictions": {"required": False, "type": "L", "list_type": "S"},
333 | "photoUrl": {
334 | "required": False,
335 | "max_length": 2048,
336 | "type": "S",
337 | },
338 | "last_edited": {"required": False, "type": "N", "max_length": 20},
339 | "verified": {"required": False, "type": "BOOL"},
340 | "latestFridgeReport": {"required": False, "type": "S"},
341 | "latestFridgeReport/epochTimestamp": {"required": False},
342 | "latestFridgeReport/timestamp": {"required": False},
343 | "latestFridgeReport/condition": {"required": False},
344 | "latestFridgeReport/foodPercentage": {"required": False},
345 | "latestFridgeReport/photoUrl": {"required": False},
346 | "latestFridgeReport/notes": {"required": False},
347 | }
348 |
349 | TABLE_NAME = f"fridge_{DB_Item.STAGE}"
350 | FOOD_ACCEPTS = [] # TODO: Fill this in
351 | FOOD_RESTRICTIONS = [] # TODO: fill this in
352 |
353 | def __init__(self, db_client: "botocore.client.DynamoDB", fridge: dict = None):
354 | super().__init__(db_client=db_client)
355 | if fridge is not None:
356 | fridge = DB_Item.process_fields(fridge)
357 | self.id: str = fridge.get("id", None)
358 | self.name: str = fridge.get("name", None)
359 | self.tags: list = fridge.get("tags", None)
360 | self.location: dict = fridge.get("location", None)
361 | self.maintainer: dict = fridge.get("maintainer", None)
362 | self.notes: str = fridge.get("notes", None)
363 | self.food_accepts: list = fridge.get("food_accepts", None)
364 | self.food_restrictions: list = fridge.get("food_restrictions", None)
365 | self.photoUrl: str = fridge.get("photoUrl", None)
366 | self.last_edited: str = fridge.get("last_edited", None)
367 | self.latestFridgeReport: dict = fridge.get("latestFridgeReport", None)
368 | self.verified: bool = fridge.get("verified", None)
369 |
370 | def get_item(self, fridgeId):
371 | key = {"id": {"S": fridgeId}}
372 | result = self.db_client.get_item(TableName=self.TABLE_NAME, Key=key)
373 | if "Item" not in result:
374 | return DB_Response(
375 | success=False, status_code=404, message="Fridge was not found"
376 | )
377 | else:
378 | json_data = result["Item"]["json_data"]["S"]
379 | return DB_Response(
380 | success=True,
381 | status_code=200,
382 | message="Successfully Found Fridge",
383 | json_data=json_data,
384 | )
385 |
386 | def get_latest_report(self, fridgeId):
387 | key = {"id": {"S": fridgeId}}
388 | result = self.db_client.get_item(TableName=self.TABLE_NAME, Key=key)
389 | if "Item" not in result:
390 | return DB_Response(
391 | success=False, status_code=404, message="Fridge was not found"
392 | )
393 | else:
394 | json_data = result["Item"]["json_data"]["S"]
395 | dict_data = json.loads(json_data)
396 | latestFridgeReport = dict_data.get("latestFridgeReport", None)
397 | response = []
398 | if latestFridgeReport is not None:
399 | response.append(latestFridgeReport)
400 | return DB_Response(
401 | success=True,
402 | status_code=200,
403 | message="Successfully Found Fridge",
404 | json_data=json.dumps(response),
405 | )
406 |
407 | def get_latest_fridge_reports(self):
408 | response = self.db_client.scan(TableName=self.TABLE_NAME)
409 | all_reports = []
410 |
411 | if "Items" in response:
412 | for item in response['Items']:
413 | json_data = item.get("json_data", {}).get("S", "{}")
414 | dict_data = json.loads(json_data)
415 | latestFridgeReport = dict_data.get("latestFridgeReport", None)
416 | if latestFridgeReport is not None:
417 | all_reports.append(latestFridgeReport)
418 |
419 | return DB_Response(
420 | success=True,
421 | status_code=200,
422 | message="Successfully found fridge reports",
423 | json_data=json.dumps(all_reports),
424 | )
425 |
426 |
427 | def get_items(self, tag=None):
428 | # scan/query doc: https://boto3.amazonaws.com/v1/documentation/api/latest/guide/dynamodb.html#querying-and-scanning
429 | response = None
430 | if tag:
431 | response = self.db_client.scan(
432 | TableName=self.TABLE_NAME,
433 | FilterExpression="contains (tags, :tag)",
434 | ExpressionAttributeValues={":tag": {"S": tag}},
435 | ProjectionExpression="json_data",
436 | )
437 | else:
438 | response = self.db_client.scan(
439 | TableName=self.TABLE_NAME, ProjectionExpression="json_data"
440 | )
441 | json_data_list = [item["json_data"]["S"] for item in response["Items"]]
442 | # Converts list of json to json.
443 | json_response = f"[{','.join(json_data_list)}]"
444 | return DB_Response(
445 | success=True,
446 | status_code=200,
447 | message="Query Successfully Completed",
448 | json_data=json_response,
449 | )
450 |
451 | def add_items(self):
452 | pass
453 |
454 | def set_id(self):
455 | """
456 | Sets the Fridge id
457 | Fridge id is the Fridge name with certain characters removed if not URL complient
458 | """
459 | if self.name is not None:
460 | id = re.sub(r"[^a-zA-Z0-9\-_~]+", "", self.name.lower())
461 | self.id = id
462 |
463 | @staticmethod
464 | def is_valid_id(fridgeId: str) -> tuple[bool, str]:
465 | """
466 | Checks if the fridge is id valid. A valid fridge id is alphanumeric and
467 | must have character length >= 3 and <= 32
468 | """
469 | if fridgeId is None:
470 | return False, "Missing Required Field: id"
471 | if re.search('[^A-Za-z0-9\-._~:/?#\[\]@!$&\'()*+,;=]', fridgeId):
472 | return False, "id has invalid characters"
473 | id_length = len(fridgeId)
474 | is_valid_id_length = Fridge.MIN_ID_LENGTH <= id_length <= Fridge.MAX_ID_LENGTH
475 | if not is_valid_id_length:
476 | return (
477 | False,
478 | f"id Must Have A Character Length >= {Fridge.MIN_ID_LENGTH} and <= {Fridge.MAX_ID_LENGTH}",
479 | )
480 | return True, "success"
481 |
482 | def set_last_edited(self):
483 | """
484 | Sets last_edited to the current epoch time. Timezone defaults to UTC
485 | """
486 | self.last_edited = str(int(time.time()))
487 |
488 | def add_item(self, conditional_expression=None) -> DB_Response:
489 | """
490 | Adds a fridge item to the database
491 | """
492 | self.set_id()
493 | self.set_last_edited()
494 | conditional_expression = "attribute_not_exists(id)"
495 | db_response = super().add_item(conditional_expression=conditional_expression)
496 | if db_response.status_code == 201:
497 | db_response.set_json_data(json.dumps({"id": self.id}))
498 | return db_response
499 |
500 | def get_fridge_locations(self):
501 | pass
502 |
503 | def update_fridge_report(self, fridgeId: str, fridge_report: dict) -> DB_Response:
504 | """
505 | Updates latestFridgeReport field with new Fridge Report.
506 | This function is called when a new FridgeReport is added to the database
507 | """
508 | db_reponse = self.get_item(fridgeId=fridgeId)
509 | if not db_reponse.is_successful():
510 | # Fridge was not found
511 | return db_reponse
512 | fridge_dict = json.loads(db_reponse.json_data)
513 | fridge_dict["latestFridgeReport"] = fridge_report
514 | fridge_json_data = json.dumps(fridge_dict)
515 | latestFridgeReport = json.dumps(fridge_report)
516 | """
517 | JD and #LFR are mapped to the values set in ExpressionAttributeNames
518 | :fj and :fr are mapped to the values set in ExpressionAttributeValues
519 | UpdateExpression becomes: "json_data": {"S": fridge_json_data}, "latestFridgeReport": {"S": latestFridgeReport}
520 | """
521 | self.db_client.update_item(
522 | TableName=self.TABLE_NAME,
523 | Key={"id": {"S": fridgeId}},
524 | ExpressionAttributeNames={"#LFR": "latestFridgeReport", "#JD": "json_data"},
525 | ExpressionAttributeValues={
526 | ":fr": {"S": latestFridgeReport},
527 | ":fj": {"S": fridge_json_data},
528 | },
529 | UpdateExpression="SET #JD = :fj, #LFR = :fr",
530 | )
531 | return DB_Response(
532 | success=True, status_code=201, message="fridge_report was succesfully added"
533 | )
534 |
535 |
536 | # good, dirty, out of order, not at location
537 | class FridgeReport(DB_Item):
538 | TABLE_NAME = f"fridge_report_{DB_Item.STAGE}"
539 | VALID_CONDITIONS = {"good", "dirty", "out of order", "not at location", "ghost"}
540 | VALID_FOOD_PERCENTAGE = {0, 1, 2, 3}
541 | FIELD_VALIDATION = {
542 | "notes": {"required": False, "max_length": 256, "type": "S"},
543 | "fridgeId": {
544 | "required": True,
545 | "min_length": Fridge.MIN_ID_LENGTH,
546 | "max_length": Fridge.MAX_ID_LENGTH,
547 | "type": "S",
548 | },
549 | "photoUrl": {
550 | "required": False,
551 | "max_length": 2048,
552 | "type": "S",
553 | },
554 | "epochTimestamp": {"required": False, "type": "N"},
555 | "timestamp": {"required": False, "type": "S"},
556 | "condition": {"required": True, "type": "S", "choices": VALID_CONDITIONS},
557 | "foodPercentage": {
558 | "required": True,
559 | "type": "N",
560 | "choices": VALID_FOOD_PERCENTAGE,
561 | },
562 | }
563 |
564 | def __init__(
565 | self, db_client: "botocore.client.DynamoDB", fridge_report: dict = None
566 | ):
567 | super().__init__(db_client=db_client)
568 | if fridge_report is not None:
569 | fridge_report = self.process_fields(fridge_report)
570 | self.notes: str = fridge_report.get("notes", None)
571 | self.condition: str = fridge_report.get("condition", None)
572 | self.photoUrl: str = fridge_report.get("photoUrl", None)
573 | self.fridgeId: str = fridge_report.get("fridgeId", None)
574 | self.foodPercentage: int = fridge_report.get("foodPercentage", None)
575 | # timestamp and epochTimestamp have the same time but in different formats
576 | self.timestamp: str = fridge_report.get(
577 | "timestamp", None
578 | ) # ISO formatted timestamp for api clients
579 | self.epochTimestamp: str = fridge_report.get(
580 | "epochTimestamp", None
581 | ) # Epoch timestamp for querying
582 |
583 | def set_timestamp(self):
584 | """
585 | Sets epochTimestamp and timestamp fields
586 | timestamp is ISO formatted date/time and is what the API user will use
587 | epochTimestamp will be what is used to query the database
588 | """
589 | self.epochTimestamp = str(int(time.time()))
590 | utc_time = datetime.datetime.utcnow()
591 | self.timestamp = utc_time.strftime("%Y-%m-%dT%H:%M:%SZ")
592 |
593 | def object_to_dict(self):
594 | """
595 | Converts object into a dictionary
596 | """
597 | object_dict = {}
598 | for key in self.FIELD_VALIDATION:
599 | val = getattr(self, key)
600 | if val is not None:
601 | object_dict[key] = val
602 | return object_dict
603 |
604 | def add_item(self) -> DB_Response:
605 | self.set_timestamp()
606 | fridge_report_dict = self.object_to_dict()
607 | db_response = super().add_item()
608 | if not db_response.is_successful():
609 | return db_response
610 | #updates the latestFridgeReport field in the Fridge table
611 | Fridge(db_client=self.db_client).update_fridge_report(fridgeId=self.fridgeId, fridge_report=fridge_report_dict)
612 | db_response.set_json_data(json.dumps({'fridgeId': self.fridgeId, 'timestamp': self.timestamp}))
613 | return db_response
614 |
615 |
616 | class FridgeHistory(DB_Item):
617 | def __init__(self, db_client: "botocore.client.DynamoDB"):
618 | super().__init__(db_client=db_client)
619 |
620 |
621 | class Tag(DB_Item):
622 | REQUIRED_FIELDS = ["tag_name"]
623 | TABLE_NAME = f"tag_{DB_Item.STAGE}"
624 | # Tag Class constants
625 | MIN_TAG_LENGTH = 3
626 | MAX_TAG_LENGTH = 32
627 |
628 | def __init__(self, db_client: "botocore.client.DynamoDB", tag_name: str = None):
629 | super().__init__(db_client=db_client)
630 | self.tag_name = self.format_tag(tag_name)
631 |
632 | def format_tag(self, tag_name: str) -> str:
633 | # tag_name is alphanumeric, all lowercased, may include hyphen and underscore but no spaces.
634 | if tag_name:
635 | tag_name = tag_name.lower().replace(" ", "")
636 | return tag_name
637 |
638 | @staticmethod
639 | def is_valid_tag_name(tag_name: str) -> Tuple[bool, str]:
640 | # valid tag name is alphanumeric, all lowercased, may include hyphen and underscore but no spaces.
641 | if tag_name is None:
642 | message = "Missing required fields: tag_name"
643 | return False, message
644 | length_tag_name = len(tag_name)
645 | is_tag_length_valid = (
646 | length_tag_name >= Tag.MIN_TAG_LENGTH
647 | and length_tag_name <= Tag.MAX_TAG_LENGTH
648 | )
649 | if tag_name and is_tag_length_valid:
650 | for x in tag_name:
651 | if not x.isalnum() and x not in ["_", "-"]:
652 | message = "tag_name contains invalid characters"
653 | return False, message
654 | message = ""
655 | return True, message
656 | else:
657 | message = f"Length of tag_name is {length_tag_name}. It should be >= {Tag.MIN_TAG_LENGTH} but <= {Tag.MAX_TAG_LENGTH}."
658 | return False, message
659 |
660 | def add_item(self) -> DB_Response:
661 | has_required_fields, field = self.has_required_fields()
662 | if not has_required_fields:
663 | return DB_Response(
664 | message=f"Missing Required Field: {field}",
665 | status_code=400,
666 | success=False,
667 | )
668 | is_valid_field = self.is_valid_tag_name(self.tag_name)
669 | if not is_valid_field:
670 | return DB_Response(
671 | message=f"Tag Name Can Only Contain Letters, Numbers, Hyphens and Underscore: {self.tag_name}",
672 | status_code=400,
673 | success=False,
674 | )
675 | item = {"tag_name": {"S": self.tag_name}}
676 |
677 | try:
678 | self.db_client.put_item(TableName=self.TABLE_NAME, Item=item)
679 | except self.db_client.exceptions.ResourceNotFoundException as e:
680 | message = f"Cannot do operations on a non-existent table: {Tag.TABLE_NAME}"
681 | logging.error(message)
682 | return DB_Response(message=message, status_code=500, success=False)
683 | except ClientError as e:
684 | logging.error(e)
685 | return DB_Response(
686 | message="Unexpected AWS service exception",
687 | status_code=500,
688 | success=False,
689 | )
690 | return DB_Response(
691 | message="Tag was succesfully added", status_code=200, success=True
692 | )
693 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/dependencies/python/requirements.txt:
--------------------------------------------------------------------------------
1 | boto3
2 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/dependencies/python/s3_service.py:
--------------------------------------------------------------------------------
1 | import os
2 | import uuid
3 | from urllib.parse import urlparse, urlunparse
4 | import logging
5 | import boto3
6 | import botocore
7 | from botocore.exceptions import ClientError
8 | from botocore.config import Config
9 |
10 | def get_s3_client(env):
11 | endpoint_url="http://localstack:4566/" if env == "local" else None
12 | config = Config(
13 | signature_version=botocore.UNSIGNED, # Do not include signatures in s3 presigned-urls.
14 | )
15 |
16 | return boto3.client(
17 | "s3",
18 | config=config,
19 | endpoint_url=endpoint_url,
20 | )
21 |
22 | def translate_s3_url_for_client(url: str, env=os.getenv("Environment")) -> str:
23 | """
24 | This function translates a S3 url to a client-accessible urls.
25 |
26 | From a lambda function's perspective, local AWS gateway is located at "localstack:4566" (within cfm-network).
27 | However, this hostname is not available for the host machine.
28 | In order to fetch S3 files from a local browser, we need to use "localhost:4566".
29 | """
30 | if env == "local":
31 | parsed_url = urlparse(url)
32 | return urlunparse(parsed_url._replace(netloc="localhost:4566"))
33 | return url
34 |
35 |
36 | class S3ServiceException(Exception):
37 | pass
38 |
39 |
40 | class S3Service:
41 | """
42 | Adapter class for persisting binary files in S3 buckets.
43 | """
44 | def __init__(self, env=os.getenv("Environment")):
45 | self._env = env
46 | self._client = get_s3_client(env)
47 |
48 | def idempotent_create_bucket(self, bucket: str):
49 | """
50 | Creates a bucket if there is no existing bucket with the same name.
51 | No-op when not local.
52 | Parameters:
53 | bucket: name of the bucket
54 | """
55 | if self._env != "local":
56 | return
57 | try:
58 | self._client.create_bucket(Bucket=bucket)
59 | except ClientError as e:
60 | logging.error(e)
61 | raise S3ServiceException(f"Failed to create a bucket {bucket}")
62 |
63 | def write(self, bucket: str, content_type: str, blob: bytes):
64 | """
65 | writes a binary file to the storage.
66 | Parameters:
67 | bucket: bucket to put file into
68 | extension: file extension
69 | blob: binary data to be written
70 | Returns:
71 | The key of the newly created file
72 | """
73 | extension = content_type.split("/")[1]
74 | key = f"{str(uuid.uuid4())}.{extension}"
75 | self.idempotent_create_bucket(bucket)
76 |
77 | try:
78 | self._client.put_object(
79 | Bucket=bucket,
80 | Key=key,
81 | Body=blob,
82 | ContentType=content_type
83 | )
84 | except ClientError as e:
85 | logging.error(e)
86 | raise S3ServiceException(f"Failed to save file {key} in bucket {bucket}")
87 |
88 | return key
89 |
90 | def generate_file_url(self, bucket: str, key: str):
91 | """
92 | generates an url for the client to access a persisted file.
93 | Parameters:
94 | bucket: name of the bucket that contains the file
95 | key: key of the file within the bucket
96 | Returns:
97 | A public url for the specified file
98 | """
99 | try:
100 | url = self._client.generate_presigned_url(
101 | "get_object",
102 | Params={
103 | "Bucket": bucket,
104 | "Key": key,
105 | },
106 | ExpiresIn=0,
107 | )
108 | except ClientError as e:
109 | logging.error(e)
110 | raise S3ServiceException(f"Failed to generate url for file {key} in bucket {bucket}")
111 |
112 | return translate_s3_url_for_client(url)
113 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/events/event.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "{\"message\": \"hello world\"}",
3 | "resource": "/hello",
4 | "path": "/hello",
5 | "httpMethod": "GET",
6 | "isBase64Encoded": false,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "pathParameters": {
11 | "proxy": "/path/to/resource"
12 | },
13 | "stageVariables": {
14 | "baz": "qux"
15 | },
16 | "headers": {
17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
18 | "Accept-Encoding": "gzip, deflate, sdch",
19 | "Accept-Language": "en-US,en;q=0.8",
20 | "Cache-Control": "max-age=0",
21 | "CloudFront-Forwarded-Proto": "https",
22 | "CloudFront-Is-Desktop-Viewer": "true",
23 | "CloudFront-Is-Mobile-Viewer": "false",
24 | "CloudFront-Is-SmartTV-Viewer": "false",
25 | "CloudFront-Is-Tablet-Viewer": "false",
26 | "CloudFront-Viewer-Country": "US",
27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
28 | "Upgrade-Insecure-Requests": "1",
29 | "User-Agent": "Custom User Agent String",
30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
33 | "X-Forwarded-Port": "443",
34 | "X-Forwarded-Proto": "https"
35 | },
36 | "requestContext": {
37 | "accountId": "123456789012",
38 | "resourceId": "123456",
39 | "stage": "prod",
40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
41 | "requestTime": "09/Apr/2015:12:34:56 +0000",
42 | "requestTimeEpoch": 1428582896000,
43 | "identity": {
44 | "cognitoIdentityPoolId": null,
45 | "accountId": null,
46 | "cognitoIdentityId": null,
47 | "caller": null,
48 | "accessKey": null,
49 | "sourceIp": "127.0.0.1",
50 | "cognitoAuthenticationType": null,
51 | "cognitoAuthenticationProvider": null,
52 | "userArn": null,
53 | "userAgent": "Custom User Agent String",
54 | "user": null
55 | },
56 | "path": "/prod/hello",
57 | "resourcePath": "/hello",
58 | "httpMethod": "POST",
59 | "apiId": "1234567890",
60 | "protocol": "HTTP/1.1"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/events/local-contact-email.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "{\"name\": \"Test User\", \"email\": \"fridgefinderapp@gmail.com\", \"subject\": \"Test Subject\", \"message\": \"Test Message\"}",
3 | "resource": "/{proxy+}",
4 | "path": "/document",
5 | "pathParameters":{},
6 | "queryStringParameters":{},
7 | "httpMethod": "POST",
8 | "isBase64Encoded": false,
9 | "headers": {
10 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
11 | "Accept-Encoding": "gzip, deflate, sdch",
12 | "Accept-Language": "en-US,en;q=0.8",
13 | "Cache-Control": "max-age=0",
14 | "CloudFront-Forwarded-Proto": "https",
15 | "CloudFront-Is-Desktop-Viewer": "true",
16 | "CloudFront-Is-Mobile-Viewer": "false",
17 | "CloudFront-Is-SmartTV-Viewer": "false",
18 | "CloudFront-Is-Tablet-Viewer": "false",
19 | "CloudFront-Viewer-Country": "US",
20 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
21 | "Upgrade-Insecure-Requests": "1",
22 | "User-Agent": "Custom User Agent String",
23 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
24 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
25 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
26 | "X-Forwarded-Port": "443",
27 | "X-Forwarded-Proto": "https"
28 | },
29 | "multiValueHeaders": {
30 | "Accept": [
31 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
32 | ],
33 | "Accept-Encoding": [
34 | "gzip, deflate, sdch"
35 | ],
36 | "Accept-Language": [
37 | "en-US,en;q=0.8"
38 | ],
39 | "Cache-Control": [
40 | "max-age=0"
41 | ],
42 | "CloudFront-Forwarded-Proto": [
43 | "https"
44 | ],
45 | "CloudFront-Is-Desktop-Viewer": [
46 | "true"
47 | ],
48 | "CloudFront-Is-Mobile-Viewer": [
49 | "false"
50 | ],
51 | "CloudFront-Is-SmartTV-Viewer": [
52 | "false"
53 | ],
54 | "CloudFront-Is-Tablet-Viewer": [
55 | "false"
56 | ],
57 | "CloudFront-Viewer-Country": [
58 | "US"
59 | ],
60 | "Host": [
61 | "0123456789.execute-api.us-east-1.amazonaws.com"
62 | ],
63 | "Upgrade-Insecure-Requests": [
64 | "1"
65 | ],
66 | "User-Agent": [
67 | "Custom User Agent String"
68 | ],
69 | "Via": [
70 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
71 | ],
72 | "X-Amz-Cf-Id": [
73 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
74 | ],
75 | "X-Forwarded-For": [
76 | "127.0.0.1, 127.0.0.2"
77 | ],
78 | "X-Forwarded-Port": [
79 | "443"
80 | ],
81 | "X-Forwarded-Proto": [
82 | "https"
83 | ]
84 | },
85 | "requestContext": {
86 | "accountId": "123456789012",
87 | "resourceId": "123456",
88 | "stage": "prod",
89 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
90 | "requestTime": "09/Apr/2015:12:34:56 +0000",
91 | "requestTimeEpoch": 1428582896000,
92 | "identity": {
93 | "cognitoIdentityPoolId": null,
94 | "accountId": null,
95 | "cognitoIdentityId": null,
96 | "caller": null,
97 | "accessKey": null,
98 | "sourceIp": "127.0.0.1",
99 | "cognitoAuthenticationType": null,
100 | "cognitoAuthenticationProvider": null,
101 | "userArn": null,
102 | "userAgent": "Custom User Agent String",
103 | "user": null
104 | },
105 | "path": "/prod/document",
106 | "resourcePath": "/{proxy+}",
107 | "httpMethod": "POST",
108 | "apiId": "1234567890",
109 | "protocol": "HTTP/1.1"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/events/local-event-get-fridge.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "",
3 | "resource": "/{proxy+}",
4 | "path": "/document",
5 | "httpMethod": "GET",
6 | "isBase64Encoded": true,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "multiValueQueryStringParameters": {
11 | "foo": [
12 | "bar"
13 | ]
14 | },
15 | "pathParameters": {
16 | "proxy": "/document",
17 | "fridgeId": "2fish5loavesfridge"
18 | },
19 | "stageVariables": {
20 | "baz": "qux"
21 | },
22 | "headers": {
23 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
24 | "Accept-Encoding": "gzip, deflate, sdch",
25 | "Accept-Language": "en-US,en;q=0.8",
26 | "Cache-Control": "max-age=0",
27 | "CloudFront-Forwarded-Proto": "https",
28 | "CloudFront-Is-Desktop-Viewer": "true",
29 | "CloudFront-Is-Mobile-Viewer": "false",
30 | "CloudFront-Is-SmartTV-Viewer": "false",
31 | "CloudFront-Is-Tablet-Viewer": "false",
32 | "CloudFront-Viewer-Country": "US",
33 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
34 | "Upgrade-Insecure-Requests": "1",
35 | "User-Agent": "Custom User Agent String",
36 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
37 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
38 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
39 | "X-Forwarded-Port": "443",
40 | "X-Forwarded-Proto": "https"
41 | },
42 | "multiValueHeaders": {
43 | "Accept": [
44 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
45 | ],
46 | "Accept-Encoding": [
47 | "gzip, deflate, sdch"
48 | ],
49 | "Accept-Language": [
50 | "en-US,en;q=0.8"
51 | ],
52 | "Cache-Control": [
53 | "max-age=0"
54 | ],
55 | "CloudFront-Forwarded-Proto": [
56 | "https"
57 | ],
58 | "CloudFront-Is-Desktop-Viewer": [
59 | "true"
60 | ],
61 | "CloudFront-Is-Mobile-Viewer": [
62 | "false"
63 | ],
64 | "CloudFront-Is-SmartTV-Viewer": [
65 | "false"
66 | ],
67 | "CloudFront-Is-Tablet-Viewer": [
68 | "false"
69 | ],
70 | "CloudFront-Viewer-Country": [
71 | "US"
72 | ],
73 | "Host": [
74 | "0123456789.execute-api.us-east-1.amazonaws.com"
75 | ],
76 | "Upgrade-Insecure-Requests": [
77 | "1"
78 | ],
79 | "User-Agent": [
80 | "Custom User Agent String"
81 | ],
82 | "Via": [
83 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
84 | ],
85 | "X-Amz-Cf-Id": [
86 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
87 | ],
88 | "X-Forwarded-For": [
89 | "127.0.0.1, 127.0.0.2"
90 | ],
91 | "X-Forwarded-Port": [
92 | "443"
93 | ],
94 | "X-Forwarded-Proto": [
95 | "https"
96 | ]
97 | },
98 | "requestContext": {
99 | "accountId": "123456789012",
100 | "resourceId": "123456",
101 | "stage": "prod",
102 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
103 | "requestTime": "09/Apr/2015:12:34:56 +0000",
104 | "requestTimeEpoch": 1428582896000,
105 | "identity": {
106 | "cognitoIdentityPoolId": null,
107 | "accountId": null,
108 | "cognitoIdentityId": null,
109 | "caller": null,
110 | "accessKey": null,
111 | "sourceIp": "127.0.0.1",
112 | "cognitoAuthenticationType": null,
113 | "cognitoAuthenticationProvider": null,
114 | "userArn": null,
115 | "userAgent": "Custom User Agent String",
116 | "user": null
117 | },
118 | "path": "/prod/document",
119 | "resourcePath": "/{proxy+}",
120 | "httpMethod": "GET",
121 | "apiId": "1234567890",
122 | "protocol": "HTTP/1.1"
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/events/local-event-get-fridges-with-tag.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "",
3 | "resource": "/{proxy+}",
4 | "path": "/document",
5 | "httpMethod": "GET",
6 | "isBase64Encoded": true,
7 | "queryStringParameters": {
8 | "tag": "tag3"
9 | },
10 | "multiValueQueryStringParameters": {
11 | "foo": [
12 | "bar"
13 | ]
14 | },
15 | "pathParameters": {
16 | "proxy": "/document"
17 | },
18 | "stageVariables": {
19 | "baz": "qux"
20 | },
21 | "headers": {
22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
23 | "Accept-Encoding": "gzip, deflate, sdch",
24 | "Accept-Language": "en-US,en;q=0.8",
25 | "Cache-Control": "max-age=0",
26 | "CloudFront-Forwarded-Proto": "https",
27 | "CloudFront-Is-Desktop-Viewer": "true",
28 | "CloudFront-Is-Mobile-Viewer": "false",
29 | "CloudFront-Is-SmartTV-Viewer": "false",
30 | "CloudFront-Is-Tablet-Viewer": "false",
31 | "CloudFront-Viewer-Country": "US",
32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
33 | "Upgrade-Insecure-Requests": "1",
34 | "User-Agent": "Custom User Agent String",
35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
38 | "X-Forwarded-Port": "443",
39 | "X-Forwarded-Proto": "https"
40 | },
41 | "multiValueHeaders": {
42 | "Accept": [
43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
44 | ],
45 | "Accept-Encoding": [
46 | "gzip, deflate, sdch"
47 | ],
48 | "Accept-Language": [
49 | "en-US,en;q=0.8"
50 | ],
51 | "Cache-Control": [
52 | "max-age=0"
53 | ],
54 | "CloudFront-Forwarded-Proto": [
55 | "https"
56 | ],
57 | "CloudFront-Is-Desktop-Viewer": [
58 | "true"
59 | ],
60 | "CloudFront-Is-Mobile-Viewer": [
61 | "false"
62 | ],
63 | "CloudFront-Is-SmartTV-Viewer": [
64 | "false"
65 | ],
66 | "CloudFront-Is-Tablet-Viewer": [
67 | "false"
68 | ],
69 | "CloudFront-Viewer-Country": [
70 | "US"
71 | ],
72 | "Host": [
73 | "0123456789.execute-api.us-east-1.amazonaws.com"
74 | ],
75 | "Upgrade-Insecure-Requests": [
76 | "1"
77 | ],
78 | "User-Agent": [
79 | "Custom User Agent String"
80 | ],
81 | "Via": [
82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
83 | ],
84 | "X-Amz-Cf-Id": [
85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
86 | ],
87 | "X-Forwarded-For": [
88 | "127.0.0.1, 127.0.0.2"
89 | ],
90 | "X-Forwarded-Port": [
91 | "443"
92 | ],
93 | "X-Forwarded-Proto": [
94 | "https"
95 | ]
96 | },
97 | "requestContext": {
98 | "accountId": "123456789012",
99 | "resourceId": "123456",
100 | "stage": "prod",
101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
102 | "requestTime": "09/Apr/2015:12:34:56 +0000",
103 | "requestTimeEpoch": 1428582896000,
104 | "identity": {
105 | "cognitoIdentityPoolId": null,
106 | "accountId": null,
107 | "cognitoIdentityId": null,
108 | "caller": null,
109 | "accessKey": null,
110 | "sourceIp": "127.0.0.1",
111 | "cognitoAuthenticationType": null,
112 | "cognitoAuthenticationProvider": null,
113 | "userArn": null,
114 | "userAgent": "Custom User Agent String",
115 | "user": null
116 | },
117 | "path": "/prod/document",
118 | "resourcePath": "/{proxy+}",
119 | "httpMethod": "GET",
120 | "apiId": "1234567890",
121 | "protocol": "HTTP/1.1"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/events/local-event-get-fridges.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "",
3 | "resource": "/{proxy+}",
4 | "path": "/document",
5 | "httpMethod": "GET",
6 | "isBase64Encoded": true,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "multiValueQueryStringParameters": {
11 | "foo": [
12 | "bar"
13 | ]
14 | },
15 | "pathParameters": {
16 | "proxy": "/document"
17 | },
18 | "stageVariables": {
19 | "baz": "qux"
20 | },
21 | "headers": {
22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
23 | "Accept-Encoding": "gzip, deflate, sdch",
24 | "Accept-Language": "en-US,en;q=0.8",
25 | "Cache-Control": "max-age=0",
26 | "CloudFront-Forwarded-Proto": "https",
27 | "CloudFront-Is-Desktop-Viewer": "true",
28 | "CloudFront-Is-Mobile-Viewer": "false",
29 | "CloudFront-Is-SmartTV-Viewer": "false",
30 | "CloudFront-Is-Tablet-Viewer": "false",
31 | "CloudFront-Viewer-Country": "US",
32 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
33 | "Upgrade-Insecure-Requests": "1",
34 | "User-Agent": "Custom User Agent String",
35 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
36 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
37 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
38 | "X-Forwarded-Port": "443",
39 | "X-Forwarded-Proto": "https"
40 | },
41 | "multiValueHeaders": {
42 | "Accept": [
43 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
44 | ],
45 | "Accept-Encoding": [
46 | "gzip, deflate, sdch"
47 | ],
48 | "Accept-Language": [
49 | "en-US,en;q=0.8"
50 | ],
51 | "Cache-Control": [
52 | "max-age=0"
53 | ],
54 | "CloudFront-Forwarded-Proto": [
55 | "https"
56 | ],
57 | "CloudFront-Is-Desktop-Viewer": [
58 | "true"
59 | ],
60 | "CloudFront-Is-Mobile-Viewer": [
61 | "false"
62 | ],
63 | "CloudFront-Is-SmartTV-Viewer": [
64 | "false"
65 | ],
66 | "CloudFront-Is-Tablet-Viewer": [
67 | "false"
68 | ],
69 | "CloudFront-Viewer-Country": [
70 | "US"
71 | ],
72 | "Host": [
73 | "0123456789.execute-api.us-east-1.amazonaws.com"
74 | ],
75 | "Upgrade-Insecure-Requests": [
76 | "1"
77 | ],
78 | "User-Agent": [
79 | "Custom User Agent String"
80 | ],
81 | "Via": [
82 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
83 | ],
84 | "X-Amz-Cf-Id": [
85 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
86 | ],
87 | "X-Forwarded-For": [
88 | "127.0.0.1, 127.0.0.2"
89 | ],
90 | "X-Forwarded-Port": [
91 | "443"
92 | ],
93 | "X-Forwarded-Proto": [
94 | "https"
95 | ]
96 | },
97 | "requestContext": {
98 | "accountId": "123456789012",
99 | "resourceId": "123456",
100 | "stage": "prod",
101 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
102 | "requestTime": "09/Apr/2015:12:34:56 +0000",
103 | "requestTimeEpoch": 1428582896000,
104 | "identity": {
105 | "cognitoIdentityPoolId": null,
106 | "accountId": null,
107 | "cognitoIdentityId": null,
108 | "caller": null,
109 | "accessKey": null,
110 | "sourceIp": "127.0.0.1",
111 | "cognitoAuthenticationType": null,
112 | "cognitoAuthenticationProvider": null,
113 | "userArn": null,
114 | "userAgent": "Custom User Agent String",
115 | "user": null
116 | },
117 | "path": "/prod/document",
118 | "resourcePath": "/{proxy+}",
119 | "httpMethod": "GET",
120 | "apiId": "1234567890",
121 | "protocol": "HTTP/1.1"
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/events/local-fridge-report-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "{\"condition\": \"good\", \"foodPercentage\": 2}",
3 | "resource": "/{proxy+}",
4 | "path": "/document",
5 | "httpMethod": "POST",
6 | "isBase64Encoded": false,
7 | "queryStringParameters": {
8 | "foo": "bar"
9 | },
10 | "multiValueQueryStringParameters": {
11 | "foo": [
12 | "bar"
13 | ]
14 | },
15 | "pathParameters": {
16 | "proxy": "/document",
17 | "fridgeId": "2fish5loavesfridge"
18 | },
19 | "stageVariables": {
20 | "baz": "qux"
21 | },
22 | "headers": {
23 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
24 | "Accept-Encoding": "gzip, deflate, sdch",
25 | "Accept-Language": "en-US,en;q=0.8",
26 | "Cache-Control": "max-age=0",
27 | "CloudFront-Forwarded-Proto": "https",
28 | "CloudFront-Is-Desktop-Viewer": "true",
29 | "CloudFront-Is-Mobile-Viewer": "false",
30 | "CloudFront-Is-SmartTV-Viewer": "false",
31 | "CloudFront-Is-Tablet-Viewer": "false",
32 | "CloudFront-Viewer-Country": "US",
33 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
34 | "Upgrade-Insecure-Requests": "1",
35 | "User-Agent": "Custom User Agent String",
36 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
37 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
38 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
39 | "X-Forwarded-Port": "443",
40 | "X-Forwarded-Proto": "https"
41 | },
42 | "multiValueHeaders": {
43 | "Accept": [
44 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
45 | ],
46 | "Accept-Encoding": [
47 | "gzip, deflate, sdch"
48 | ],
49 | "Accept-Language": [
50 | "en-US,en;q=0.8"
51 | ],
52 | "Cache-Control": [
53 | "max-age=0"
54 | ],
55 | "CloudFront-Forwarded-Proto": [
56 | "https"
57 | ],
58 | "CloudFront-Is-Desktop-Viewer": [
59 | "true"
60 | ],
61 | "CloudFront-Is-Mobile-Viewer": [
62 | "false"
63 | ],
64 | "CloudFront-Is-SmartTV-Viewer": [
65 | "false"
66 | ],
67 | "CloudFront-Is-Tablet-Viewer": [
68 | "false"
69 | ],
70 | "CloudFront-Viewer-Country": [
71 | "US"
72 | ],
73 | "Host": [
74 | "0123456789.execute-api.us-east-1.amazonaws.com"
75 | ],
76 | "Upgrade-Insecure-Requests": [
77 | "1"
78 | ],
79 | "User-Agent": [
80 | "Custom User Agent String"
81 | ],
82 | "Via": [
83 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
84 | ],
85 | "X-Amz-Cf-Id": [
86 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
87 | ],
88 | "X-Forwarded-For": [
89 | "127.0.0.1, 127.0.0.2"
90 | ],
91 | "X-Forwarded-Port": [
92 | "443"
93 | ],
94 | "X-Forwarded-Proto": [
95 | "https"
96 | ]
97 | },
98 | "requestContext": {
99 | "accountId": "123456789012",
100 | "resourceId": "123456",
101 | "stage": "prod",
102 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
103 | "requestTime": "09/Apr/2015:12:34:56 +0000",
104 | "requestTimeEpoch": 1428582896000,
105 | "identity": {
106 | "cognitoIdentityPoolId": null,
107 | "accountId": null,
108 | "cognitoIdentityId": null,
109 | "caller": null,
110 | "accessKey": null,
111 | "sourceIp": "127.0.0.1",
112 | "cognitoAuthenticationType": null,
113 | "cognitoAuthenticationProvider": null,
114 | "userArn": null,
115 | "userAgent": "Custom User Agent String",
116 | "user": null
117 | },
118 | "path": "/prod/document",
119 | "resourcePath": "/{proxy+}",
120 | "httpMethod": "POST",
121 | "apiId": "1234567890",
122 | "protocol": "HTTP/1.1"
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/events/local-post-fridge-event.json:
--------------------------------------------------------------------------------
1 | {
2 | "body": "{\"name\": \"The Greenpoint Fridge\",\"location\": {\"address\": \"9 W. Elm St.\", \"geoLat\": \"40.730610\", \"geoLng\": \"-73.935242\"}}",
3 | "resource": "/{proxy+}",
4 | "path": "/document",
5 | "pathParameters":{},
6 | "queryStringParameters":{},
7 | "httpMethod": "POST",
8 | "isBase64Encoded": false,
9 | "headers": {
10 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
11 | "Accept-Encoding": "gzip, deflate, sdch",
12 | "Accept-Language": "en-US,en;q=0.8",
13 | "Cache-Control": "max-age=0",
14 | "CloudFront-Forwarded-Proto": "https",
15 | "CloudFront-Is-Desktop-Viewer": "true",
16 | "CloudFront-Is-Mobile-Viewer": "false",
17 | "CloudFront-Is-SmartTV-Viewer": "false",
18 | "CloudFront-Is-Tablet-Viewer": "false",
19 | "CloudFront-Viewer-Country": "US",
20 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
21 | "Upgrade-Insecure-Requests": "1",
22 | "User-Agent": "Custom User Agent String",
23 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
24 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
25 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
26 | "X-Forwarded-Port": "443",
27 | "X-Forwarded-Proto": "https"
28 | },
29 | "multiValueHeaders": {
30 | "Accept": [
31 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
32 | ],
33 | "Accept-Encoding": [
34 | "gzip, deflate, sdch"
35 | ],
36 | "Accept-Language": [
37 | "en-US,en;q=0.8"
38 | ],
39 | "Cache-Control": [
40 | "max-age=0"
41 | ],
42 | "CloudFront-Forwarded-Proto": [
43 | "https"
44 | ],
45 | "CloudFront-Is-Desktop-Viewer": [
46 | "true"
47 | ],
48 | "CloudFront-Is-Mobile-Viewer": [
49 | "false"
50 | ],
51 | "CloudFront-Is-SmartTV-Viewer": [
52 | "false"
53 | ],
54 | "CloudFront-Is-Tablet-Viewer": [
55 | "false"
56 | ],
57 | "CloudFront-Viewer-Country": [
58 | "US"
59 | ],
60 | "Host": [
61 | "0123456789.execute-api.us-east-1.amazonaws.com"
62 | ],
63 | "Upgrade-Insecure-Requests": [
64 | "1"
65 | ],
66 | "User-Agent": [
67 | "Custom User Agent String"
68 | ],
69 | "Via": [
70 | "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
71 | ],
72 | "X-Amz-Cf-Id": [
73 | "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
74 | ],
75 | "X-Forwarded-For": [
76 | "127.0.0.1, 127.0.0.2"
77 | ],
78 | "X-Forwarded-Port": [
79 | "443"
80 | ],
81 | "X-Forwarded-Proto": [
82 | "https"
83 | ]
84 | },
85 | "requestContext": {
86 | "accountId": "123456789012",
87 | "resourceId": "123456",
88 | "stage": "prod",
89 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
90 | "requestTime": "09/Apr/2015:12:34:56 +0000",
91 | "requestTimeEpoch": 1428582896000,
92 | "identity": {
93 | "cognitoIdentityPoolId": null,
94 | "accountId": null,
95 | "cognitoIdentityId": null,
96 | "caller": null,
97 | "accessKey": null,
98 | "sourceIp": "127.0.0.1",
99 | "cognitoAuthenticationType": null,
100 | "cognitoAuthenticationProvider": null,
101 | "userArn": null,
102 | "userAgent": "Custom User Agent String",
103 | "user": null
104 | },
105 | "path": "/prod/document",
106 | "resourcePath": "/{proxy+}",
107 | "httpMethod": "POST",
108 | "apiId": "1234567890",
109 | "protocol": "HTTP/1.1"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/README.md:
--------------------------------------------------------------------------------
1 | Directories under the **dev** directory will eventually get deleted
2 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/hello_world/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/hello_world/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/hello_world/app.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | try:
4 | from db import layer_test
5 | except:
6 | from dependencies.python.db import layer_test
7 |
8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers
9 | # Here is an example of someone having a similar issue:
10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror
11 |
12 |
13 | def lambda_handler(event, context):
14 | """Sample pure Lambda function
15 |
16 | Parameters
17 | ----------
18 | event: dict, required
19 | API Gateway Lambda Proxy Input Format
20 |
21 | Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
22 |
23 | context: object, required
24 | Lambda Context runtime methods and attributes
25 |
26 | Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html
27 |
28 | Returns
29 | ------
30 | API Gateway Lambda Proxy Output Format: dict
31 |
32 | Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
33 | """
34 |
35 | return {
36 | "statusCode": 200,
37 | "body": json.dumps(
38 | {
39 | "message": layer_test(),
40 | }
41 | ),
42 | }
43 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/hello_world/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/hello_world/requirements.txt
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/load_fridge_data/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/load_fridge_data/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/load_fridge_data/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import logging
4 | from botocore.exceptions import ClientError
5 | from db import get_ddb_connection
6 | from db import Fridge
7 |
8 | logger = logging.getLogger()
9 | logger.setLevel(logging.INFO)
10 |
11 | FRIDGE_DATA = [
12 | {
13 | "name": "The Friendly Fridge",
14 | "location": {
15 | "geoLat": 124242,
16 | "geoLng": 2345235,
17 | "street": "1046 Broadway",
18 | "zip": "11221",
19 | "state": "NY",
20 | "city": "Brooklyn",
21 | "name": "test name",
22 | },
23 | "tags": ["tag1", "tag3"],
24 | "maintainer": {"instagram": "https://www.instagram.com/thefriendlyfridge/"},
25 | },
26 | {
27 | "name": "2 Fish 5 Loaves Fridge",
28 | "location": {
29 | "geoLat": 40.701730,
30 | "geoLng": -73.944530,
31 | "street": "63 Whipple St",
32 | "zip": "11206",
33 | "state": "NY",
34 | "city": "Brooklyn",
35 | "name": "test name",
36 | },
37 | "tags": ["tag1", "tag4"],
38 | "maintainer": {"instagram": "https://www.instagram.com/2fish5loavesfridge/"},
39 | },
40 | ]
41 |
42 |
43 | FRIDGE_REPORT_DATA = []
44 | FRIDGE_HISTORY_DATA = []
45 |
46 |
47 | def lambda_handler(
48 | event: dict, context: "awslambdaric.lambda_context.LambdaContext"
49 | ) -> dict:
50 | db_client = get_ddb_connection(env=os.environ["Environment"])
51 | try:
52 | responses = []
53 | response = None
54 | for fridge in FRIDGE_DATA:
55 | response = Fridge(fridge=fridge, db_client=db_client).add_item()
56 | responses.append(response.get_dict_form())
57 | if response.status_code != 201:
58 | break
59 |
60 | return {
61 | "statusCode": response.status_code,
62 | "isBase64Encoded": "false",
63 | "headers": {"Content-Type": "application/json"},
64 | "body": json.dumps(
65 | {
66 | "responses": responses,
67 | }
68 | ),
69 | }
70 |
71 | except db_client.exceptions.ResourceNotFoundException as e:
72 | logging.error("Table does not exist")
73 | raise e
74 | except ClientError as e:
75 | logging.error("Unexpected error")
76 | raise e
77 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/dev/load_fridge_data/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/dev/load_fridge_data/requirements.txt
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/email_service/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/email_service/v1/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/email_service/v1/app.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import json
3 | from re import sub
4 | import boto3
5 | import json
6 | import logging
7 | from botocore.exceptions import ClientError
8 |
9 | logger = logging.getLogger()
10 | logger.setLevel(logging.INFO)
11 |
12 | RECIPIENT = "fridgefinderapp@gmail.com"
13 | SENDER = "fridgefinderapp@gmail.com"
14 |
15 |
16 | def api_response(status_code, body) -> dict:
17 | return {
18 | "statusCode": status_code,
19 | "headers": {
20 | "Content-Type": "application/json",
21 | "Access-Control-Allow-Origin": "*",
22 | },
23 | "body": body,
24 | }
25 |
26 |
27 | class SendEmail:
28 |
29 | CHARSET = "UTF-8"
30 |
31 | def __init__(self, client=boto3.client("ses", "us-east-1")):
32 | self.client = client
33 |
34 | def sendEmail(self, sender, recipient, subject, body) -> int:
35 | try:
36 | # Provide the contents of the email.
37 | response = self.client.send_email(
38 | Source=sender,
39 | Destination={
40 | "ToAddresses": [recipient],
41 | },
42 | Message={
43 | "Body": {
44 | "Html": {
45 | "Data": body,
46 | },
47 | "Text": {
48 | "Charset": SendEmail.CHARSET,
49 | "Data": body,
50 | },
51 | },
52 | "Subject": {
53 | "Charset": SendEmail.CHARSET,
54 | "Data": subject,
55 | },
56 | }
57 | # we can use a configuationset for logging purposest
58 | # maybe we'd want to define the names in a list of constants/enums?
59 | # ConfigurationSetName=configuration_set,
60 | )
61 | # Display an error if something goes wrong.
62 | except ClientError as e:
63 | logger.error(e.response["Error"]["Message"])
64 | return api_response(404, "Unable to send Email. Please try again")
65 | else:
66 | messageId = response.get("MessageId", "Not Found")
67 | logger.info(f"Email sent! Message ID: {messageId}")
68 | return api_response(
69 | 200, json.dumps({"message": "Succesffully Sent Email!"})
70 | )
71 |
72 |
73 | class ContactHandler:
74 | def extract_body_fields(body):
75 | body = json.loads(body)
76 | sender = body.get("name", None)
77 | senderEmailAddress = body.get("email", None)
78 | subject = body.get("subject", "No Subject")
79 | subject = f"FridgeFinder Contact: {subject}."
80 | message = body.get("message", None)
81 | if subject and sender:
82 | subject = f"{subject} From: {sender}"
83 | return message, subject, sender, senderEmailAddress
84 |
85 | @staticmethod
86 | def lambda_handler(event: dict, client=boto3.client("ses", "us-east-1")) -> dict:
87 | body = event.get("body", {})
88 | if not body:
89 | return api_response(
90 | 400,
91 | json.dumps(
92 | {"message": "Unable to send Email. Missing Required Fields"}
93 | ),
94 | )
95 | (
96 | message,
97 | subject,
98 | senderName,
99 | senderEmailAddress,
100 | ) = ContactHandler.extract_body_fields(body)
101 | if not senderName or not senderEmailAddress or not message:
102 | return api_response(
103 | "400",
104 | json.dumps(
105 | {"message": "Unable to send Email. Missing Required Fields"}
106 | ),
107 | )
108 | response = SendEmail(client).sendEmail(
109 | SENDER,
110 | RECIPIENT,
111 | subject,
112 | ContactHandler.format_email(senderEmailAddress, message, senderName),
113 | )
114 | return response
115 |
116 | @staticmethod
117 | def format_email(email, body, name):
118 | return f"From: {name} {email}\r\nMessage: {body}"
119 |
120 |
121 | def lambda_handler(
122 | event: dict, context: "awslambdaric.lambda_context.LambdaContext"
123 | ) -> dict:
124 | return ContactHandler.lambda_handler(event)
125 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/fridge_reports/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/fridge_reports/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/fridge_reports/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | import json
4 |
5 | try:
6 | from db import get_ddb_connection, FridgeReport, Fridge, DB_Response
7 | except:
8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers
9 | # Here is an example of someone having a similar issue:
10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror
11 | from dependencies.python.db import (
12 | get_ddb_connection,
13 | FridgeReport,
14 | Fridge,
15 | DB_Response,
16 | )
17 |
18 | logger = logging.getLogger()
19 | logger.setLevel(logging.INFO)
20 |
21 |
22 | class FridgReportHandler:
23 | def __init__():
24 | pass
25 |
26 | @staticmethod
27 | def lambda_handler(event: dict, ddbclient: "botocore.client.DynamoDB") -> dict:
28 | """
29 | Extracts the necessary data from events dict, and executes a function corresponding
30 | to the event httpMethod
31 | """
32 | body = event.get("body", None)
33 | fridge_id = event.get("pathParameters", {}).get("fridgeId", None)
34 | if body is not None:
35 | body = json.loads(body)
36 | body["fridgeId"] = fridge_id
37 | httpMethod = event.get("httpMethod", None)
38 | db_response = None
39 | if httpMethod == "POST":
40 | db_response = FridgeReport(
41 | db_client=ddbclient, fridge_report=body
42 | ).add_item()
43 | elif httpMethod == "GET":
44 | db_response = Fridge(db_client=ddbclient).get_latest_report(
45 | fridgeId=fridge_id
46 | )
47 | else:
48 | FridgeReport(db_client=ddbclient).warm_lambda()
49 | db_response = DB_Response(False, 400, "httpMethod missing")
50 | return db_response.api_format()
51 |
52 |
53 | def lambda_handler(
54 | event: dict, context: "awslambdaric.lambda_context.LambdaContext"
55 | ) -> dict:
56 | env = os.environ["Environment"]
57 | ddbclient = get_ddb_connection(env)
58 | return FridgReportHandler.lambda_handler(event, ddbclient)
59 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/fridge_reports/get_latest_fridge_reports/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/fridge_reports/get_latest_fridge_reports/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/fridge_reports/get_latest_fridge_reports/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | import json
4 |
5 | try:
6 | from db import get_ddb_connection, Fridge, DB_Response
7 | except:
8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers
9 | # Here is an example of someone having a similar issue:
10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror
11 | from dependencies.python.db import (
12 | get_ddb_connection,
13 | Fridge,
14 | DB_Response,
15 | )
16 |
17 | logger = logging.getLogger()
18 | logger.setLevel(logging.INFO)
19 |
20 |
21 | class GetLatestFridgeReportHandler:
22 | def __init__():
23 | pass
24 |
25 | @staticmethod
26 | def lambda_handler(event: dict, ddbclient: "botocore.client.DynamoDB") -> dict:
27 | """
28 | Extracts the necessary data from events dict, and executes a function corresponding
29 | to the event httpMethod
30 | """
31 | httpMethod = event.get("httpMethod", None)
32 | db_response = None
33 | if httpMethod == "GET":
34 | db_response = Fridge(db_client=ddbclient).get_latest_fridge_reports()
35 | else:
36 | db_response = DB_Response(False, 400, "httpMethod missing")
37 | return db_response.api_format()
38 |
39 |
40 | def lambda_handler(
41 | event: dict, context: "awslambdaric.lambda_context.LambdaContext"
42 | ) -> dict:
43 | env = os.environ["Environment"]
44 | ddbclient = get_ddb_connection(env)
45 | return GetLatestFridgeReportHandler.lambda_handler(event, ddbclient)
46 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/fridges/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/fridges/v1/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/fridges/v1/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | import json
4 |
5 | try:
6 | from db import get_ddb_connection, Fridge, DB_Response
7 | except:
8 | # If it gets here it's because we are performing a unit test. It's a common error when using lambda layers
9 | # Here is an example of someone having a similar issue:
10 | # https://stackoverflow.com/questions/69592094/pytest-failing-in-aws-sam-project-due-to-modulenotfounderror
11 | from dependencies.python.db import get_ddb_connection, Fridge, DB_Response
12 |
13 | logger = logging.getLogger()
14 | logger.setLevel(logging.INFO)
15 |
16 |
17 | class FridgeHandler:
18 | @staticmethod
19 | def lambda_handler(event: dict, ddbclient: "botocore.client.DynamoDB") -> dict:
20 | """
21 | Extracts the necessary data from events dict, and executes a function corresponding
22 | to the event httpMethod
23 | """
24 | httpMethod = event.get("httpMethod", None)
25 | body = event.get("body", None)
26 | pathParameters = event.get("pathParameters", None) or {}
27 | queryStringParameters = event.get("queryStringParameters", None) or {}
28 | tag = queryStringParameters.get("tag", None)
29 | fridgeId = pathParameters.get("fridgeId", None)
30 | db_response = None
31 | if httpMethod == "GET":
32 | if fridgeId:
33 | db_response = Fridge(db_client=ddbclient).get_item(fridgeId)
34 | else:
35 | db_response = Fridge(db_client=ddbclient).get_items(tag=tag)
36 | elif httpMethod == "POST":
37 | if body is not None:
38 | body = json.loads(body)
39 | db_response = Fridge(db_client=ddbclient, fridge=body).add_item()
40 | else:
41 | Fridge(db_client=ddbclient).warm_lambda()
42 | db_response = DB_Response(False, 400, "httpMethod missing")
43 | return db_response.api_format()
44 |
45 |
46 | def lambda_handler(
47 | event: dict, context: "awslambdaric.lambda_context.LambdaContext"
48 | ) -> dict:
49 | env = os.environ["Environment"]
50 | ddbclient = get_ddb_connection(env)
51 | return FridgeHandler.lambda_handler(event, ddbclient)
52 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/image/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/functions/image/v1/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/functions/image/v1/app.py:
--------------------------------------------------------------------------------
1 | import json
2 | import base64
3 | import os
4 |
5 | try:
6 | from s3_service import S3Service
7 | except:
8 | from dependencies.python.s3_service import S3Service
9 |
10 |
11 | def api_response(body, status_code) -> dict:
12 | return {
13 | "statusCode": status_code,
14 | "headers": {
15 | "Content-Type": "application/json",
16 | "Access-Control-Allow-Origin": "*",
17 | },
18 | "body": (body),
19 | }
20 |
21 | def get_s3_bucket_name():
22 | STAGE = os.getenv("Stage", None)
23 | bucket = "community-fridge-map-images"
24 | if STAGE is not None:
25 | bucket = f"{bucket}-{STAGE}"
26 | return bucket
27 |
28 | class ImageHandler:
29 |
30 | def get_image_content_type(image_data: str):
31 | """
32 | Checks if the given image data represents a valid image and returns its content type.
33 |
34 | :param image_data: The binary data of the image.
35 | :return: The content type of the image if it is a valid image format, None otherwise.
36 | """
37 | signatures = {
38 | b'\xff\xd8': 'image/jpeg',
39 | b'\x89PNG\r\n\x1a\n': 'image/png',
40 | b'GIF87a': 'image/gif',
41 | b'GIF89a': 'image/gif',
42 | b'II*\x00': 'image/tiff',
43 | b'MM\x00*': 'image/tiff',
44 | # Add more signatures and content types as needed
45 | }
46 |
47 | webp_markers = [b'VP8 ', b'VP8L', b'VP8X']
48 |
49 | for signature, content_type in signatures.items():
50 | if image_data.startswith(signature):
51 | return content_type
52 |
53 | # Check for WebP format separately, as it requires additional checks
54 | for marker in webp_markers:
55 | if marker in image_data[:16]:
56 | return 'image/webp'
57 |
58 | return None
59 |
60 | @staticmethod
61 | def get_binary_body_from_event(event: dict) -> bytes:
62 | """Extract binary data from request body"""
63 | if event["isBase64Encoded"]:
64 | return base64.b64decode(event["body"])
65 | else:
66 | return None
67 |
68 | @staticmethod
69 | def encode_binary_file_for_response(blob: bytes) -> bytes:
70 | """
71 | Binary response body of lambda functions should be encoded in base64.
72 | https://docs.aws.amazon.com/apigateway/latest/developerguide/lambda-proxy-binary-media.html
73 | """
74 | return base64.b64encode(blob)
75 |
76 |
77 | @staticmethod
78 | def lambda_handler(event: dict, s3: S3Service) -> dict:
79 | if event.get("body", None) is None:
80 | error_message = {"message": "Received an empty body"}
81 | return api_response(body=json.dumps(error_message), status_code=400)
82 | if not event.get('isBase64Encoded', False):
83 | error_message = {"message": "Must be Base64 Encoded"}
84 | return api_response(body=json.dumps(error_message), status_code=400)
85 |
86 | bucket = get_s3_bucket_name()
87 | image_data = ImageHandler.get_binary_body_from_event(event)
88 | content_type = ImageHandler.get_image_content_type(image_data)
89 | if content_type is None:
90 | error_message = json.dumps({"message": "Invalid Image Format"})
91 | return api_response(body=error_message, status_code=400)
92 | try:
93 | key = s3.write(bucket, content_type, image_data)
94 | url = s3.generate_file_url(bucket, key)
95 | except:
96 | error_message: str = json.dumps({"message": "Unexpected error prevented server from fulfilling request."})
97 | return api_response(body={"message": error_message}, status_code=500)
98 | body = json.dumps({"photoUrl": url})
99 | return api_response(body=body, status_code=200)
100 |
101 |
102 | def lambda_handler(
103 | event: dict, context: "awslambdaric.lambda_context.LambdaContext"
104 | ) -> dict:
105 | s3 = S3Service()
106 | return ImageHandler.lambda_handler(event, s3)
107 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | boto3
4 | pre-commit
5 | coverage
6 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/template.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: "2010-09-09"
2 | Transform: AWS::Serverless-2016-10-31
3 | Description: >
4 | CommunityFridgeMapApi
5 |
6 | SAM Template for CommunityFridgeMapApi
7 |
8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
9 | Globals:
10 | Api:
11 | BinaryMediaTypes:
12 | - image/webp
13 | Function:
14 | Timeout: 60
15 |
16 | Parameters:
17 | Environment:
18 | Type: String
19 | Description: Choose between local or AWS
20 | AllowedValues:
21 | - local
22 | - aws
23 | Stage:
24 | Type: String
25 | Description: Choose between dev, staging, prod
26 | AllowedValues:
27 | - dev
28 | - staging
29 | - prod
30 | CFMHostedZoneId:
31 | Type: String
32 | Description: Grab the HostedZoneId from Route53 console
33 |
34 | ##########################
35 | ## Resources ##
36 | ##########################
37 | Resources:
38 | ApiCertificate:
39 | Type: AWS::CertificateManager::Certificate
40 | Properties:
41 | DomainName: !Sub api-${Stage}.communityfridgefinder.com
42 | DomainValidationOptions:
43 | - DomainName: !Sub api-${Stage}.communityfridgefinder.com
44 | HostedZoneId: !Sub ${CFMHostedZoneId}
45 | ValidationMethod: DNS
46 | ApiGatewayApi:
47 | Type: AWS::Serverless::Api
48 | Properties:
49 | StageName: !Ref Stage
50 | # CORS preflight settings from https://vercel.com/guides/how-to-enable-cors
51 | Cors:
52 | AllowMethods: "'GET,OPTIONS,PATCH,DELETE,POST,PUT'"
53 | AllowHeaders: "'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length,
54 | Content-MD5, Content-Type, Date, X-Api-Version'"
55 | AllowOrigin: "'*'"
56 | MaxAge: "'86400'"
57 | AllowCredentials: false
58 | EndpointConfiguration: REGIONAL
59 | Domain:
60 | DomainName: !Sub api-${Stage}.communityfridgefinder.com
61 | CertificateArn: !Ref ApiCertificate
62 | Route53:
63 | HostedZoneName: "communityfridgefinder.com." # NOTE: The period at the end is required
64 |
65 | FridgeReportFunction:
66 | Type: AWS::Serverless::Function
67 | Properties:
68 | CodeUri: functions/fridge_reports
69 | Handler: app.lambda_handler
70 | Runtime: python3.9
71 | FunctionName: !Sub FridgeReportFunction${Stage}
72 | Environment:
73 | Variables:
74 | Environment: !Ref Environment
75 | Stage: !Ref Stage
76 | Layers:
77 | - !Ref CommonLayer
78 | Policies:
79 | - Version: "2012-10-17"
80 | Statement:
81 | - Effect: Allow
82 | Action:
83 | - logs:CreateLogGroup
84 | - logs:CreateLogStream
85 | - logs:PutLogEvents
86 | Resource: arn:aws:logs:*:*:*
87 | - Effect: Allow
88 | Action:
89 | - dynamodb:PutItem
90 | Resource: !GetAtt FridgeReportTable.Arn
91 | - Effect: Allow
92 | Action:
93 | - dynamodb:GetItem
94 | - dynamodb:UpdateItem
95 | Resource: !GetAtt FridgeTable.Arn
96 | Events:
97 | PostFridgeReport:
98 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
99 | Properties:
100 | Path: /v1/fridges/{fridgeId}/reports
101 | Method: post
102 | RestApiId:
103 | Ref: ApiGatewayApi
104 | RequestParameters:
105 | - method.request.path.fridgeId:
106 | Required: true
107 | Caching: false
108 | GetFridgeReport:
109 | Type: Api
110 | Properties:
111 | Path: /v1/fridges/{fridgeId}/reports
112 | Method: get
113 | RestApiId:
114 | Ref: ApiGatewayApi
115 | RequestParameters:
116 | - method.request.path.fridgeId:
117 | Required: true
118 | Caching: false
119 | FridgeReportWarmUp:
120 | Type: Schedule
121 | Properties:
122 | # Read more about schedule expressions here:
123 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html
124 | # This event runs every 10 minutes
125 | Schedule: rate(1 minute)
126 |
127 | GetLatestFridgeReportsFunction:
128 | Type: AWS::Serverless::Function
129 | Properties:
130 | CodeUri: functions/fridge_reports/get_latest_fridge_reports
131 | Handler: app.lambda_handler
132 | Runtime: python3.9
133 | FunctionName: !Sub GetLatestFridgeReportsFunction${Stage}
134 | Environment:
135 | Variables:
136 | Environment: !Ref Environment
137 | Stage: !Ref Stage
138 | Layers:
139 | - !Ref CommonLayer
140 | Policies:
141 | - Version: "2012-10-17"
142 | Statement:
143 | - Effect: Allow
144 | Action:
145 | - logs:CreateLogGroup
146 | - logs:CreateLogStream
147 | - logs:PutLogEvents
148 | Resource: arn:aws:logs:*:*:*
149 | - Effect: Allow
150 | Action:
151 | - dynamodb:Scan
152 | Resource: !GetAtt FridgeTable.Arn
153 | Events:
154 | GetLatestFridgeReports:
155 | Type: Api
156 | Properties:
157 | Path: /v1/reports/last
158 | Method: get
159 | RestApiId:
160 | Ref: ApiGatewayApi
161 |
162 | HelloWorldFunction:
163 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
164 | Properties:
165 | FunctionName: !Sub HelloWorldFunction${Stage}
166 | CodeUri: functions/dev/hello_world/
167 | Handler: app.lambda_handler
168 | Runtime: python3.9
169 | Layers:
170 | - !Ref CommonLayer
171 | Events:
172 | HelloWorld:
173 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
174 | Properties:
175 | Path: /hello
176 | Method: get
177 | RestApiId:
178 | Ref: ApiGatewayApi
179 | Description: Responds with 'hello world' if successful.
180 |
181 | FridgesFunction:
182 | Type: AWS::Serverless::Function
183 | Properties:
184 | CodeUri: functions/fridges/v1
185 | Handler: app.lambda_handler
186 | Runtime: python3.9
187 | FunctionName: !Sub FridgesFunction${Stage}
188 | Environment:
189 | Variables:
190 | Environment: !Ref Environment
191 | Stage: !Ref Stage
192 | Layers:
193 | - !Ref CommonLayer
194 | Policies:
195 | - Version: "2012-10-17"
196 | Statement:
197 | - Effect: Allow
198 | Action:
199 | - logs:CreateLogGroup
200 | - logs:CreateLogStream
201 | - logs:PutLogEvents
202 | Resource: arn:aws:logs:*:*:*
203 | - Effect: Allow
204 | Action:
205 | - dynamodb:GetItem
206 | - dynamodb:Scan
207 | - dynamodb:PutItem
208 | Resource: !GetAtt FridgeTable.Arn
209 | Events:
210 | GetFridge:
211 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
212 | Properties:
213 | Path: /v1/fridges/{fridgeId}
214 | Method: get
215 | RequestParameters:
216 | - method.request.path.fridgeId:
217 | Required: false
218 | Caching: false
219 | RestApiId:
220 | Ref: ApiGatewayApi
221 | GetFridges:
222 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
223 | Properties:
224 | Path: /v1/fridges
225 | Method: get
226 | RequestParameters:
227 | - method.request.querystring.tag:
228 | Required: false
229 | RestApiId:
230 | Ref: ApiGatewayApi
231 | PostFridge:
232 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
233 | Properties:
234 | Path: /v1/fridges
235 | Method: post
236 | RestApiId:
237 | Ref: ApiGatewayApi
238 | FridgeWarmUp:
239 | Type: Schedule
240 | Properties:
241 | # Read more about schedule expressions here:
242 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html
243 | # This event runs every 10 minutes
244 | Schedule: rate(1 minute)
245 |
246 | ImageFunction:
247 | Type: AWS::Serverless::Function
248 | Properties:
249 | CodeUri: functions/image/v1
250 | Handler: app.lambda_handler
251 | Runtime: python3.9
252 | FunctionName: !Sub ImageFunction${Stage}
253 | Environment:
254 | Variables:
255 | Environment: !Ref Environment
256 | Stage: !Ref Stage
257 | Layers:
258 | - !Ref CommonLayer
259 | Policies:
260 | - Version: '2012-10-17'
261 | Statement:
262 | - Effect: Allow
263 | Action:
264 | - logs:CreateLogGroup
265 | - logs:CreateLogStream
266 | - logs:PutLogEvents
267 | Resource: arn:aws:logs:*:*:*
268 | Statement:
269 | - Effect: Allow
270 | Action:
271 | - s3:PutObject
272 | - s3:GetObject
273 | Resource:
274 | - !Sub "arn:aws:s3:::${CommunityFridgeMapImages}/*"
275 | - !Sub "arn:aws:s3:::${CommunityFridgeMapImages}"
276 | Events:
277 | UploadImage:
278 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
279 | Properties:
280 | Path: /v1/photo
281 | Method: post
282 | RestApiId:
283 | Ref: ApiGatewayApi
284 | ImageWarmUp:
285 | Type: Schedule
286 | Properties:
287 | # Read more about schedule expressions here:
288 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html
289 | # This event runs every 10 minutes
290 | Schedule: rate(10 minutes)
291 |
292 | ContactFunction:
293 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
294 | Properties:
295 | CodeUri: functions/email_service/v1
296 | Handler: app.lambda_handler
297 | FunctionName: !Sub ContactFunction${Stage}
298 | Environment:
299 | Variables:
300 | Environment: !Ref Environment
301 | Stage: !Ref Stage
302 | Runtime: python3.9
303 | Layers:
304 | - !Ref CommonLayer
305 | Policies:
306 | - Version: "2012-10-17"
307 | Statement:
308 | - Effect: Allow
309 | Action:
310 | - logs:CreateLogGroup
311 | - logs:CreateLogStream
312 | - logs:PutLogEvents
313 | Resource: arn:aws:logs:*:*:*
314 | - SESCrudPolicy: { IdentityName: fridgefinderapp@gmail.com }
315 |
316 | Events:
317 | SendSESFunction:
318 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
319 | Properties:
320 | Path: /v1/contact
321 | Method: post
322 | RestApiId:
323 | Ref: ApiGatewayApi
324 | ContactWarmUp:
325 | Type: Schedule
326 | Properties:
327 | # Read more about schedule expressions here:
328 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html
329 | # This event runs every 10 minutes
330 | Schedule: rate(10 minutes)
331 |
332 | LoadFridgeDataFunction: #FOR DEV TESTING PURPOSES ONLY. TODO: DELETE THIS FUNCTION
333 | Type: AWS::Serverless::Function
334 | Properties:
335 | CodeUri: ./functions/dev/load_fridge_data
336 | Handler: app.lambda_handler
337 | Runtime: python3.9
338 | FunctionName: !Sub LoadFridgeDataFunction${Stage}
339 | Environment:
340 | Variables:
341 | Environment: !Ref Environment
342 | Stage: !Ref Stage
343 | Layers:
344 | - !Ref CommonLayer
345 | Policies:
346 | - Version: "2012-10-17"
347 | Statement:
348 | - Effect: Allow
349 | Action:
350 | - logs:CreateLogGroup
351 | - logs:CreateLogStream
352 | - logs:PutLogEvents
353 | Resource: arn:aws:logs:*:*:*
354 | - Effect: Allow
355 | Action:
356 | - dynamodb:PutItem
357 | Resource: !GetAtt FridgeTable.Arn
358 | ##########################
359 | ## Layers ##
360 | ##########################
361 | CommonLayer:
362 | Type: AWS::Serverless::LayerVersion
363 | Properties:
364 | LayerName: !Sub CommunityFridgeMapApiLayer${Stage}
365 | Description: Dependencies for CommunityFridgeMapApi project
366 | ContentUri: dependencies/
367 | CompatibleRuntimes:
368 | - python3.7
369 | - python3.8
370 | - python3.9
371 | RetentionPolicy: Delete
372 | ##########################
373 | ## Bucket Policies ##
374 | ##########################
375 | ImageFunctionBucketPolicy:
376 | Type: AWS::S3::BucketPolicy
377 | Properties:
378 | Bucket: !Ref CommunityFridgeMapImages
379 | PolicyDocument:
380 | Statement:
381 | - Action:
382 | - s3:*
383 | Effect: Allow
384 | Resource:
385 | - !Sub arn:aws:s3:::${CommunityFridgeMapImages}
386 | - !Sub arn:aws:s3:::${CommunityFridgeMapImages}/*
387 | Principal:
388 | AWS:
389 | - '*'
390 | ##########################
391 | ## S3 Buckets ##
392 | ##########################
393 | CommunityFridgeMapImages:
394 | Type: AWS::S3::Bucket
395 | Properties:
396 | BucketName: !Sub community-fridge-map-images-${Stage}
397 | CorsConfiguration:
398 | CorsRules:
399 | - AllowedHeaders:
400 | - "*"
401 | AllowedMethods:
402 | - GET
403 | - PUT
404 | - HEAD
405 | AllowedOrigins:
406 | - "*"
407 | ##########################
408 | ## DynamoDB Tables ##
409 | ##########################
410 |
411 | FridgeTable:
412 | Type: AWS::DynamoDB::Table
413 | Properties:
414 | AttributeDefinitions:
415 | - AttributeName: "id"
416 | AttributeType: "S"
417 | KeySchema:
418 | - AttributeName: "id"
419 | KeyType: "HASH"
420 | TableName: !Sub fridge_${Stage}
421 | BillingMode: "PAY_PER_REQUEST"
422 |
423 | FridgeReportTable:
424 | Type: AWS::DynamoDB::Table
425 | Properties:
426 | AttributeDefinitions:
427 | - AttributeName: "fridgeId"
428 | AttributeType: "S"
429 | - AttributeName: "epochTimestamp" #epoch time used for querying data
430 | AttributeType: "N"
431 | KeySchema:
432 | - AttributeName: "fridgeId"
433 | KeyType: "HASH"
434 | - AttributeName: "epochTimestamp"
435 | KeyType: "RANGE"
436 | TableName: !Sub fridge_report_${Stage}
437 | BillingMode: "PAY_PER_REQUEST"
438 |
439 | TagTable:
440 | Type: AWS::DynamoDB::Table
441 | Properties:
442 |
443 | AttributeDefinitions:
444 | - AttributeName: "tag_name"
445 | AttributeType: "S"
446 | KeySchema:
447 | - AttributeName: "tag_name"
448 | KeyType: "HASH"
449 | TableName: !Sub tag_${Stage}
450 | BillingMode: "PAY_PER_REQUEST"
451 |
452 | FridgeHistoryTable:
453 | Type: AWS::DynamoDB::Table
454 | Properties:
455 | AttributeDefinitions:
456 | - AttributeName: "fridgeId"
457 | AttributeType: "S"
458 | - AttributeName: "epochTimestamp" #epoch time used for querying data
459 | AttributeType: "N"
460 | KeySchema:
461 | - AttributeName: "fridgeId"
462 | KeyType: "HASH"
463 | - AttributeName: "epochTimestamp"
464 | KeyType: "RANGE"
465 | TableName: !Sub fridge_history_${Stage}
466 | BillingMode: "PAY_PER_REQUEST"
467 |
468 |
469 | Outputs:
470 | HelloWorldFunction:
471 | Description: "Hello World Lambda Function ARN"
472 | Value: !GetAtt HelloWorldFunction.Arn
473 | HelloWorldFunctionIamRole:
474 | Description: "Implicit IAM Role created for Hello World function"
475 | Value: !GetAtt HelloWorldFunctionRole.Arn
476 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/tests/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/assert_resposne.py:
--------------------------------------------------------------------------------
1 | import json
2 | from typing import Union, Optional
3 |
4 | def assert_response(
5 | response: dict,
6 | status: Optional[int]=None,
7 | headers: Optional[dict]=None,
8 | body: Union[str, bytes, dict, None]=None,
9 | ):
10 | """
11 | Make assertions against a lambda response.
12 |
13 | Only specified parameters are checked.
14 | Parameters:
15 | status: expected status code
16 | headers: expected headers
17 | body: expected body. It can be a string, dict (json), or (base64 encoded) bytes.
18 | """
19 | if status is not None:
20 | assert response["statusCode"] == status
21 | if headers is not None:
22 | assert response["headers"] == headers
23 | if body is not None:
24 | if type(body) == str or type(body) == bytes:
25 | assert response["body"] == body
26 | elif type(body) == dict:
27 | assert json.loads(response["body"]) == body
28 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from tests.s3_service_stub import S3ServiceStub
3 |
4 | @pytest.fixture
5 | def s3_service_stub():
6 | return S3ServiceStub()
7 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/tests/integration/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/integration/test_api_gateway.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import TestCase
3 |
4 | import boto3
5 | import requests
6 |
7 | """
8 | Make sure env variable AWS_SAM_STACK_NAME exists with the name of the stack we are going to test.
9 | """
10 |
11 |
12 | class TestApiGateway(TestCase):
13 | api_endpoint: str
14 |
15 | @classmethod
16 | def get_stack_name(cls) -> str:
17 | stack_name = os.environ.get("AWS_SAM_STACK_NAME")
18 | if not stack_name:
19 | raise Exception(
20 | "Cannot find env var AWS_SAM_STACK_NAME. \n"
21 | "Please setup this environment variable with the stack name where we are running integration tests."
22 | )
23 |
24 | return stack_name
25 |
26 | def setUp(self) -> None:
27 | """
28 | Based on the provided env variable AWS_SAM_STACK_NAME,
29 | here we use cloudformation API to find out what the HelloWorldApi URL is
30 | """
31 | stack_name = TestApiGateway.get_stack_name()
32 |
33 | client = boto3.client("cloudformation")
34 |
35 | try:
36 | response = client.describe_stacks(StackName=stack_name)
37 | except Exception as e:
38 | raise Exception(
39 | f"Cannot find stack {stack_name}. \n"
40 | f'Please make sure stack with the name "{stack_name}" exists.'
41 | ) from e
42 |
43 | stacks = response["Stacks"]
44 |
45 | stack_outputs = stacks[0]["Outputs"]
46 | api_outputs = [
47 | output for output in stack_outputs if output["OutputKey"] == "HelloWorldApi"
48 | ]
49 | self.assertTrue(
50 | api_outputs, f"Cannot find output HelloWorldApi in stack {stack_name}"
51 | )
52 |
53 | self.api_endpoint = api_outputs[0]["OutputValue"]
54 |
55 | def test_api_gateway(self):
56 | """
57 | Call the API Gateway endpoint and check the response
58 | """
59 | response = requests.get(self.api_endpoint)
60 | self.assertDictEqual(response.json(), {"message": "hello world"})
61 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | pytest-mock
3 | boto3
4 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/s3_service_stub.py:
--------------------------------------------------------------------------------
1 | class S3ServiceStub:
2 | """Stub implementation of the Storage class for testing"""
3 | def __init__(self):
4 | self._buckets = {}
5 | self._current_index = 0
6 |
7 | def idempotent_create_bucket(self, bucket: str):
8 | if bucket not in self._buckets:
9 | self._buckets[bucket] = {}
10 |
11 | def read(self, bucket: str, key: str):
12 | return self._buckets[bucket][key]
13 |
14 | def write(self, bucket: str, extension: str, blob: bytes):
15 | key = f"{self._current_index}.{extension}"
16 | self._current_index += 1
17 | self.idempotent_create_bucket(bucket)
18 | self._buckets[bucket][key] = blob
19 | return key
20 |
21 | def generate_file_url(self, bucket: str, key: str):
22 | return f"http://localhost:4566/{bucket}/{key}"
23 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Backend/4894fd2611947f001a1c0d82b15b90d4febc55bc/CommunityFridgeMapApi/tests/unit/__init__.py
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_db_item.py:
--------------------------------------------------------------------------------
1 | from CommunityFridgeMapApi.dependencies.python.db import DB_Item
2 |
3 | import unittest
4 |
5 |
6 | class DB_Item_Test(unittest.TestCase):
7 | def test_remove_extra_whitespace(self):
8 | result = DB_Item.remove_extra_whitespace(" h i th er h o w ")
9 | self.assertEqual(result, "h i th er h o w")
10 |
11 | def test_process_fields(self):
12 | object_dict = {"1": " h i ", "2": {"test": " h i "}, "3": [" h i "]}
13 | result = DB_Item.process_fields(object_dict=object_dict)
14 | self.assertEqual(result, {"1": "h i", "2": {"test": "h i"}, "3": ["h i"]})
15 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_db_response.py:
--------------------------------------------------------------------------------
1 | from CommunityFridgeMapApi.dependencies.python.db import DB_Response
2 |
3 | import unittest
4 |
5 |
6 | class DB_Response_Test(unittest.TestCase):
7 | def test_api_format(self):
8 | db_response = DB_Response(success=True, status_code=201, message="testing")
9 | api_format = db_response.api_format()
10 | self.assertEqual(api_format["statusCode"], 201)
11 | self.assertEqual(api_format["body"], '{"message": "testing"}')
12 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_email_service.py:
--------------------------------------------------------------------------------
1 | from http import client
2 | from CommunityFridgeMapApi.functions.email_service.v1.app import ContactHandler
3 | from dependencies.python.db import get_ddb_connection
4 | import unittest
5 | from dependencies.python.db import Fridge
6 | import json
7 |
8 |
9 | class SESClientMock:
10 | def send_email(
11 | self,
12 | Source,
13 | Destination,
14 | Message,
15 | ):
16 | return {"MessageId": 1}
17 |
18 |
19 | class ContactHandlerTest(unittest.TestCase):
20 | def test_format_email(self):
21 | formatted_email = ContactHandler.format_email("email", "body", "name")
22 | self.assertEqual(formatted_email, "From: name email\r\nMessage: body")
23 |
24 | def test_lambda_handler_no_body(self):
25 | response = ContactHandler.lambda_handler({}, client=SESClientMock())
26 | self.assertEqual(response["statusCode"], 400)
27 | body = json.loads(response["body"])
28 | self.assertEqual(
29 | body, {"message": "Unable to send Email. Missing Required Fields"}
30 | )
31 |
32 | def test_lambda_handler_missing_fields(self):
33 | event = {"body": {}}
34 | response = ContactHandler.lambda_handler(event, client=SESClientMock())
35 | body = json.loads(response["body"])
36 | self.assertEqual(
37 | body, {"message": "Unable to send Email. Missing Required Fields"}
38 | )
39 |
40 | def test_extract_body_fields(self):
41 | body = json.dumps(
42 | {
43 | "name": "name",
44 | "email": "email",
45 | "subject": "subject",
46 | "message": "message",
47 | }
48 | )
49 | (
50 | message,
51 | subject,
52 | sender,
53 | senderEmailAddress,
54 | ) = ContactHandler.extract_body_fields(body)
55 | self.assertEqual(message, "message")
56 | self.assertEqual(sender, "name")
57 | self.assertEqual(subject, "FridgeFinder Contact: subject. From: name")
58 | self.assertEqual(senderEmailAddress, "email")
59 |
60 | def test_lambda_handler(self):
61 | body = json.dumps(
62 | {
63 | "name": "name",
64 | "email": "email",
65 | "subject": "subject",
66 | "message": "message",
67 | }
68 | )
69 | event = {"body": body}
70 | response = ContactHandler.lambda_handler(event=event, client=SESClientMock())
71 | self.assertEqual(response["statusCode"], 200)
72 | message = json.loads(response["body"])["message"]
73 | self.assertEqual(message, "Succesffully Sent Email!")
74 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_fridge.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from CommunityFridgeMapApi.dependencies.python.db import FridgeReport
3 | from CommunityFridgeMapApi.functions.fridges.v1.app import FridgeHandler
4 | from dependencies.python.db import layer_test
5 | from dependencies.python.db import get_ddb_connection
6 | import unittest
7 | from dependencies.python.db import Fridge
8 | import json
9 | import os
10 |
11 |
12 | def test_layer_test():
13 |
14 | ret = layer_test()
15 | assert ret == "hello world"
16 |
17 |
18 | def test_get_ddb_connection():
19 | os.environ['AWS_DEFAULT_REGION'] = 'us-west-2'
20 | connection = get_ddb_connection()
21 | assert str(type(connection)) == ""
22 | connection = get_ddb_connection(env="local")
23 | assert str(type(connection)) == ""
24 |
25 |
26 | class DynamoDbMockPutItem:
27 | def __init__(self):
28 | pass
29 |
30 | def put_item(self, TableName=None, Item=None, ConditionExpression=None):
31 | pass
32 |
33 |
34 | class DynamoDbMockScan:
35 | def __init__(self):
36 | pass
37 |
38 | def scan(
39 | self,
40 | TableName=None,
41 | FilterExpression=None,
42 | ExpressionAttributeValues=None,
43 | ProjectionExpression=None,
44 | ):
45 | return {"Items": [{"json_data": {"S": '{"id": {"S": "test"}}'}}]}
46 |
47 |
48 | class FridgeTest(unittest.TestCase):
49 | def test_set_id(self):
50 | fridge = Fridge(fridge={"name": "The Friendly Fridge"}, db_client=None)
51 | fridge.set_id()
52 | self.assertEqual(fridge.id, "thefriendlyfridge")
53 |
54 | def test_set_last_edditted(self):
55 | fridge = Fridge(fridge={"name": "The Friendly Fridge"}, db_client=None)
56 | self.assertIsNone(fridge.last_edited)
57 | fridge.set_last_edited()
58 | self.assertIsNotNone(fridge.last_edited)
59 |
60 | def test_add_item_with_invalid_id_characters(self):
61 | """
62 | This is testing to see if the special characters are removed
63 | """
64 | db_client = DynamoDbMockPutItem()
65 | fridge = Fridge(
66 | fridge={
67 | "name": "fridgàe&^ñ@(*#(&.(*$<>.#%{}|\^~[]\";:/?@=&$+,",
68 | "location": {"geoLat": 124242, "geoLng": 2345235},
69 | },
70 | db_client=db_client,
71 | )
72 | response = fridge.add_item()
73 | self.assertTrue(response.success)
74 | self.assertEqual(
75 | fridge.id, "fridge~"
76 | )
77 | self.assertEqual(response.status_code, 201)
78 |
79 | def test_add_item_missing_required_field(self):
80 | db_client = DynamoDbMockPutItem()
81 | fridge = Fridge(
82 | fridge={
83 | "name": "test fridge",
84 | "location": {"geoLat": 12124},
85 | },
86 | db_client=db_client,
87 | )
88 | response = fridge.add_item()
89 | self.assertFalse(response.success)
90 | self.assertEqual(response.message, "Missing Required Field: location/geoLng")
91 | self.assertEqual(response.status_code, 400)
92 |
93 | def test_add_item_min_length_failure(self):
94 | db_client = DynamoDbMockPutItem()
95 | fridge = Fridge(
96 | fridge={
97 | "name": "it",
98 | "location": {"geoLat": 12124, "geoLng": 232523},
99 | },
100 | db_client=db_client,
101 | )
102 | response = fridge.add_item()
103 | self.assertFalse(response.success)
104 | self.assertEqual(response.message, "id character length must be >= 3")
105 | self.assertEqual(response.status_code, 400)
106 |
107 | def test_add_item_success(self):
108 | db_client = DynamoDbMockPutItem()
109 | fridge = Fridge(
110 | fridge={
111 | "name": "Test Fridge",
112 | "location": {"geoLat": 124242, "geoLng": 2345235},
113 | },
114 | db_client=db_client,
115 | )
116 | response = fridge.add_item()
117 | self.assertTrue(response.success)
118 | self.assertEqual(response.json_data, json.dumps({"id": "testfridge"}))
119 | self.assertEqual(response.status_code, 201)
120 | self.assertIsNotNone(fridge.last_edited)
121 |
122 | def test_format_dynamodb_item_v2(self):
123 | fridge = {
124 | "id": "test",
125 | "name": "test",
126 | "tags": ["tag3"],
127 | "location": {},
128 | "maintainer": {},
129 | "notes": "test",
130 | "food_accepts": ["dairy"],
131 | "food_restrictions": ["meat"],
132 | "photoUrl": "test",
133 | "last_edited": 2342353,
134 | "verified": True,
135 | "latestFridgeReport": {},
136 | }
137 | fridge_item = Fridge(fridge=fridge, db_client=None).format_dynamodb_item_v2()
138 | expected_response = {
139 | "id": {"S": "test"},
140 | "name": {"S": "test"},
141 | "tags": {"L": [{"S": "tag3"}]},
142 | "location": {"S": "{}"},
143 | "maintainer": {"S": "{}"},
144 | "notes": {"S": "test"},
145 | "food_accepts": {"L": [{"S": "dairy"}]},
146 | "food_restrictions": {"L": [{"S": "meat"}]},
147 | "photoUrl": {"S": "test"},
148 | "last_edited": {"N": "2342353"},
149 | "verified": {"BOOL": True},
150 | "latestFridgeReport": {"S": "{}"},
151 | "json_data": {"S": json.dumps(fridge)},
152 | }
153 | self.assertEqual(fridge_item, expected_response)
154 | fridge_item = Fridge(
155 | fridge={"id": ""}, db_client=None
156 | ).format_dynamodb_item_v2()
157 | self.assertEqual(fridge_item, {"json_data": {"S": "{}"}})
158 |
159 | def test_is_valid_id(self):
160 | is_valid, message = Fridge.is_valid_id(None)
161 | self.assertEqual(message, "Missing Required Field: id")
162 | self.assertFalse(is_valid)
163 |
164 | is_valid, message = Fridge.is_valid_id("hi there")
165 | self.assertEqual(message, "id has invalid characters")
166 | self.assertFalse(is_valid)
167 |
168 | is_valid, message = Fridge.is_valid_id("hi")
169 | self.assertEqual(message, "id Must Have A Character Length >= 3 and <= 100")
170 | self.assertFalse(is_valid)
171 |
172 | def test_validate_fields_required_fields(self):
173 | fridge = Fridge(fridge={}, db_client=None)
174 | field_validator = fridge.validate_fields()
175 | self.assertFalse(field_validator.is_valid)
176 | self.assertEqual(field_validator.message, "Missing Required Field: id")
177 |
178 | def test_validate_fields_min_length(self):
179 | fridge = Fridge(fridge={"id": "x"}, db_client=None)
180 | field_validator = fridge.validate_fields()
181 | self.assertFalse(field_validator.is_valid)
182 | self.assertEqual(field_validator.message, "id character length must be >= 3")
183 |
184 | def test_validate_fields_max_length(self):
185 | fridge = Fridge(
186 | fridge={
187 | "id": "x" * 101,
188 | "name": "x" * 101,
189 | "location": {"geoLat": "3", "geoLng": "3"},
190 | },
191 | db_client=None,
192 | )
193 | field_validator = fridge.validate_fields()
194 | self.assertFalse(field_validator.is_valid)
195 | self.assertEqual(field_validator.message, "id character length must be <= 100")
196 |
197 | def test_validate_fields_success(self):
198 | fridge = Fridge(
199 | fridge={
200 | "id": "goodAZaz09_#-‘'.",
201 | "name": "goodAZaz09_#-‘'. ",
202 | "location": {"geoLat": 232342, "geoLng": 2342342},
203 | },
204 | db_client=None,
205 | )
206 | field_validator = fridge.validate_fields()
207 | self.assertTrue(field_validator.is_valid)
208 | self.assertEqual(
209 | field_validator.message, "All Fields Were Successfully Validated"
210 | )
211 |
212 | def test_get_items(self):
213 | response = Fridge(db_client=DynamoDbMockScan()).get_items()
214 | expected_response = '[{"id": {"S": "test"}}]'
215 | self.assertTrue(response.is_successful())
216 | self.assertEqual(response.status_code, 200)
217 | self.assertEqual(response.json_data, expected_response)
218 |
219 | def test_get_items_tag(self):
220 | response = Fridge(db_client=DynamoDbMockScan()).get_items(tag="tag_test")
221 | expected_response = '[{"id": {"S": "test"}}]'
222 | self.assertTrue(response.is_successful())
223 | self.assertEqual(response.status_code, 200)
224 | self.assertEqual(response.json_data, expected_response)
225 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_fridge_handler.py:
--------------------------------------------------------------------------------
1 | from CommunityFridgeMapApi.functions.fridges.v1.app import FridgeHandler
2 | from dependencies.python.db import layer_test
3 | from dependencies.python.db import get_ddb_connection
4 | import unittest
5 |
6 |
7 | class DynamoDbMockGetItem:
8 | def __init__(self):
9 | pass
10 |
11 | def get_item(self, TableName=None, Key=None):
12 | return {"Item": {"json_data": {"S": '{"id": {"S": "test"}}'}}}
13 |
14 | class DynamoDbMockGetItemFail:
15 | def __init__(self):
16 | pass
17 |
18 | def get_item(self, TableName=None, Key=None):
19 | return {}
20 |
21 |
22 | class DynamoDbMockScan:
23 | def __init__(self):
24 | pass
25 |
26 | def scan(
27 | self,
28 | TableName=None,
29 | FilterExpression=None,
30 | ExpressionAttributeValues=None,
31 | ProjectionExpression=None,
32 | ):
33 | return {"Items": [{"json_data": {"S": '{"id": {"S": "test"}}'}}]}
34 |
35 |
36 | class FridgeHandlerTest(unittest.TestCase):
37 | def test_get_item_success(self):
38 | json_data = '{"id": {"S": "test"}}'
39 | response = FridgeHandler.lambda_handler(
40 | event={
41 | "httpMethod": "GET",
42 | "pathParameters": {"fridgeId": "test"},
43 | "queryStringParameters": None,
44 | },
45 | ddbclient=DynamoDbMockGetItem(),
46 | )
47 | self.assertEqual(response["statusCode"], 200)
48 | self.assertEqual(response["body"], json_data)
49 |
50 | def test_get_item_failure(self):
51 | response = FridgeHandler.lambda_handler(
52 | event={
53 | "httpMethod": "GET",
54 | "pathParameters": {"fridgeId": "hi"},
55 | "queryStringParameters": None,
56 | },
57 | ddbclient=DynamoDbMockGetItemFail(),
58 | )
59 | self.assertEqual(response["statusCode"], 404)
60 | self.assertEqual(
61 | response["body"],
62 | '{"message": "Fridge was not found"}',
63 | )
64 |
65 | def test_get_items(self):
66 | json_data = '[{"id": {"S": "test"}}]'
67 | response = FridgeHandler.lambda_handler(
68 | event={
69 | "httpMethod": "GET",
70 | "pathParameters": None,
71 | "queryStringParameters": None,
72 | },
73 | ddbclient=DynamoDbMockScan(),
74 | )
75 | self.assertEqual(response["statusCode"], 200)
76 | self.assertEqual(response["body"], json_data)
77 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_fridge_report.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from dependencies.python.db import FridgeReport
3 | import json
4 |
5 | class DynamoDbMockPutItem:
6 | def __init__(self):
7 | pass
8 |
9 | def put_item(self, TableName=None, Item=None, ConditionExpression=None):
10 | pass
11 |
12 | def get_item(self, TableName=None, Key=None):
13 | return {"Item": {"json_data": {"S": '{"id": {"S": "test"}}'}}}
14 |
15 | def update_item(
16 | self,
17 | TableName=None,
18 | Key=None,
19 | ExpressionAttributeNames=None,
20 | ExpressionAttributeValues=None,
21 | UpdateExpression=None,
22 | ):
23 | pass
24 |
25 |
26 | class FrdgeReportTest(unittest.TestCase):
27 | def test_set_timestamp(self):
28 | fridge_report = FridgeReport(
29 | fridge_report={
30 | "fridgeId": "test",
31 | "condition": "good",
32 | "notes": "all good out here",
33 | "foodPercentage": 2,
34 | },
35 | db_client=None,
36 | )
37 | fridge_report.set_timestamp()
38 | self.assertIsNotNone(fridge_report.epochTimestamp)
39 | self.assertIsNotNone(fridge_report.timestamp)
40 |
41 | def test_validate_fields(self):
42 | test_data = [
43 | {
44 | "fridge_report": {
45 | "notes": "all good",
46 | "fridgeId": "thefriendlyfridge",
47 | "photoUrl": "s3.url",
48 | "epochTimestamp": 34234,
49 | "timestamp": "good",
50 | "condition": "good",
51 | "foodPercentage": 3,
52 | },
53 | "valid": True,
54 | "message": "All Fields Were Successfully Validated",
55 | },
56 | {
57 | "fridge_report": {},
58 | "valid": False,
59 | "message": "Missing Required Field: fridgeId",
60 | },
61 | {
62 | "fridge_report": {"fridgeId": "thefriendlyfridge"},
63 | "valid": False,
64 | "message": "Missing Required Field: condition",
65 | },
66 | {
67 | "fridge_report": {"fridgeId": "thefriendlyfridge", "condition": "fake"},
68 | "valid": False,
69 | "message": "condition must to be one of",
70 | },
71 | {
72 | "fridge_report": {
73 | "fridgeId": "thefriendlyfridge",
74 | "condition": "good",
75 | },
76 | "valid": False,
77 | "message": "Missing Required Field: foodPercentage",
78 | },
79 | {
80 | "fridge_report": {
81 | "fridgeId": "thefriendlyfridge",
82 | "condition": "good",
83 | "foodPercentage": 99,
84 | },
85 | "valid": False,
86 | "message": "foodPercentage must to be one of",
87 | },
88 | {
89 | "fridge_report": {
90 | "fridgeId": "thefriendlyfridge",
91 | "condition": "good",
92 | "foodPercentage": 3,
93 | },
94 | "valid": True,
95 | "message": "All Fields Were Successfully Validated",
96 | },
97 | {
98 | "fridge_report": {
99 | "fridgeId": "thefriendlyfridge",
100 | "condition": "good",
101 | "foodPercentage": 3,
102 | "notes": "x" * 257,
103 | },
104 | "valid": False,
105 | "message": "notes character length must be <= 256",
106 | },
107 | {
108 | "fridge_report": {
109 | "fridgeId": "thefriendlyfridge",
110 | "condition": "good",
111 | "foodPercentage": 3,
112 | "photoUrl": "x" * 2049,
113 | },
114 | "valid": False,
115 | "message": "photoUrl character length must be <= 2048",
116 | },
117 | ]
118 |
119 | for data in test_data:
120 | validator = FridgeReport(
121 | db_client=None, fridge_report=data["fridge_report"]
122 | ).validate_fields()
123 | self.assertEqual(validator.is_valid, validator.is_valid)
124 | self.assertTrue(data["message"] in validator.message)
125 |
126 | def test_add_item(self):
127 | test_data = [
128 | {
129 | "fridge_report": {},
130 | "message": "Missing Required Field: fridgeId",
131 | "success": False,
132 | },
133 | {
134 | "fridge_report": {
135 | "fridgeId": "hi",
136 | "condition": "good",
137 | "foodPercentage": 2,
138 | },
139 | "message": "fridgeId character length must be >= 3",
140 | "success": False,
141 | },
142 | {
143 | "fridge_report": {
144 | "fridgeId": "valid",
145 | "condition": "hacking",
146 | "foodPercentage": 2,
147 | },
148 | "message": "condition must to be one of",
149 | "success": False,
150 | },
151 | {
152 | "fridge_report": {
153 | "fridgeId": "valid",
154 | "condition": "good",
155 | "foodPercentage": 50,
156 | },
157 | "message": "foodPercentage must to be one of",
158 | "success": False,
159 | },
160 | {
161 | "fridge_report": {
162 | "fridgeId": "test",
163 | "condition": "good",
164 | "foodPercentage": 2,
165 | "notes": "t" * 257,
166 | },
167 | "message": "notes character length must be <= 256",
168 | "success": False,
169 | },
170 | {
171 | "fridge_report": {
172 | "fridgeId": "test",
173 | "condition": "good",
174 | "foodPercentage": 2,
175 | },
176 | "fridgeId": "test",
177 | "success": True,
178 | },
179 | ]
180 |
181 | for data in test_data:
182 | response = FridgeReport(
183 | fridge_report=data["fridge_report"], db_client=DynamoDbMockPutItem()
184 | ).add_item()
185 | self.assertEqual(response.success, data["success"])
186 | if "message" in data:
187 | self.assertTrue(data["message"] in response.message)
188 | else:
189 | response_dict_data = json.loads(response.json_data)
190 | self.assertEqual(response_dict_data['fridgeId'], data['fridgeId'])
191 | self.assertTrue("timestamp" in response_dict_data)
192 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_fridge_report_handler.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | import pytest
4 |
5 | from CommunityFridgeMapApi.functions.fridge_reports.app import FridgReportHandler
6 | import json
7 |
8 |
9 | class DynamoDbMockPutItem:
10 | def __init__(self):
11 | pass
12 |
13 | def put_item(self, TableName=None, Item=None, ConditionExpression=None):
14 | pass
15 |
16 | def get_item(self, TableName=None, Key=None):
17 | return {"Item": {"json_data": {"S": '{"id": {"S": "test"}}'}}}
18 |
19 | def update_item(
20 | self,
21 | TableName=None,
22 | Key=None,
23 | ExpressionAttributeNames=None,
24 | ExpressionAttributeValues=None,
25 | UpdateExpression=None,
26 | ):
27 | pass
28 |
29 |
30 | class FrdgeReportHandlerTest(unittest.TestCase):
31 | def test_lambda_handler_success(self):
32 | event = {
33 | "body": '{"condition": "good", "foodPercentage": 2}',
34 | "pathParameters": {"fridgeId": "thefriendlyfridge"},
35 | "httpMethod": "POST",
36 | }
37 | response = FridgReportHandler.lambda_handler(
38 | event=event, ddbclient=DynamoDbMockPutItem()
39 | )
40 | self.assertEqual(response["statusCode"], 201)
41 | body = json.loads(response["body"])
42 | self.assertEqual(body["fridgeId"], "thefriendlyfridge")
43 | self.assertTrue("timestamp" in body)
44 |
45 | def test_lambda_handler_fail(self):
46 | event = {
47 | "body": '{"condition": "good"}',
48 | "pathParameters": {"fridgeId": "thefriendlyfridge"},
49 | "httpMethod": "POST",
50 | }
51 | response = FridgReportHandler.lambda_handler(
52 | event=event, ddbclient=DynamoDbMockPutItem()
53 | )
54 | self.assertEqual(response["statusCode"], 400)
55 | message = json.loads(response["body"])["message"]
56 | self.assertEqual(message, "Missing Required Field: foodPercentage")
57 |
58 | def test_lambda_handler_exception(self):
59 | event = {
60 | "body": '{"condition": "good", "foodPercentage": 2}',
61 | "pathParameters": {"fridgeId": "thefriendlyfridge"},
62 | }
63 | response = FridgReportHandler.lambda_handler(
64 | event=event, ddbclient=DynamoDbMockPutItem()
65 | )
66 | self.assertEqual(response["statusCode"], 400)
67 | message = json.loads(response["body"])["message"]
68 | self.assertEqual(message, "httpMethod missing")
69 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_get_latest_fridge_report.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from dependencies.python.db import Fridge
3 | import json
4 |
5 | class DynamoDbMockScanItem:
6 | def __init__(self):
7 | pass
8 |
9 | def scan(
10 | self,
11 | TableName=None,
12 | FilterExpression=None,
13 | ExpressionAttributeValues=None,
14 | ProjectionExpression=None,
15 | ):
16 | return {'Items': [{'json_data': {'S': '{"latestFridgeReport": {"fridgeId": "2fish5loavesfridge", "epochTimestamp": "1685405973", "timestamp": "2023-05-30T00:19:33Z", "condition": "good", "foodPercentage": 2}}'}},
17 | {'json_data': {'S': '{"latestFridgeReport": {"fridgeId": "thefriendlyfridge", "epochTimestamp": "1685406040", "timestamp": "2023-05-30T00:20:40Z", "condition": "dirty", "foodPercentage": 1}}'}}]}
18 |
19 |
20 | class FrdgeReportTest(unittest.TestCase):
21 |
22 | def test_get_latest_fridge_report(self):
23 | fridge = Fridge(db_client=DynamoDbMockScanItem())
24 | response = fridge.get_latest_fridge_reports()
25 | json_data = json.loads(response.json_data)
26 | self.assertEqual(response.message, "Successfully found fridge reports")
27 | self.assertEqual(len(json_data), 2)
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_hello_world.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | import pytest
4 |
5 | from functions.dev.hello_world import app
6 |
7 |
8 | @pytest.fixture()
9 | def apigw_event():
10 | """Generates API GW Event"""
11 |
12 | return {
13 | "body": '{ "test": "body"}',
14 | "resource": "/{proxy+}",
15 | "requestContext": {
16 | "resourceId": "123456",
17 | "apiId": "1234567890",
18 | "resourcePath": "/{proxy+}",
19 | "httpMethod": "POST",
20 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
21 | "accountId": "123456789012",
22 | "identity": {
23 | "apiKey": "",
24 | "userArn": "",
25 | "cognitoAuthenticationType": "",
26 | "caller": "",
27 | "userAgent": "Custom User Agent String",
28 | "user": "",
29 | "cognitoIdentityPoolId": "",
30 | "cognitoIdentityId": "",
31 | "cognitoAuthenticationProvider": "",
32 | "sourceIp": "127.0.0.1",
33 | "accountId": "",
34 | },
35 | "stage": "prod",
36 | },
37 | "queryStringParameters": {"foo": "bar"},
38 | "headers": {
39 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
40 | "Accept-Language": "en-US,en;q=0.8",
41 | "CloudFront-Is-Desktop-Viewer": "true",
42 | "CloudFront-Is-SmartTV-Viewer": "false",
43 | "CloudFront-Is-Mobile-Viewer": "false",
44 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
45 | "CloudFront-Viewer-Country": "US",
46 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
47 | "Upgrade-Insecure-Requests": "1",
48 | "X-Forwarded-Port": "443",
49 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
50 | "X-Forwarded-Proto": "https",
51 | "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==",
52 | "CloudFront-Is-Tablet-Viewer": "false",
53 | "Cache-Control": "max-age=0",
54 | "User-Agent": "Custom User Agent String",
55 | "CloudFront-Forwarded-Proto": "https",
56 | "Accept-Encoding": "gzip, deflate, sdch",
57 | },
58 | "pathParameters": {"proxy": "/examplepath"},
59 | "httpMethod": "POST",
60 | "stageVariables": {"baz": "qux"},
61 | "path": "/examplepath",
62 | }
63 |
64 |
65 | def test_lambda_handler(apigw_event, mocker):
66 |
67 | ret = app.lambda_handler(apigw_event, "")
68 | data = json.loads(ret["body"])
69 |
70 | assert ret["statusCode"] == 200
71 | assert "message" in ret["body"]
72 | assert data["message"] == "hello world"
73 | # assert "location" in data.dict_keys()
74 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_image.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import base64
3 | from unittest.mock import patch, ANY
4 | from functions.image.v1.app import ImageHandler
5 | from tests.assert_resposne import assert_response
6 | from tests.s3_service_stub import S3ServiceStub
7 |
8 |
9 | def test_upload(s3_service_stub: S3ServiceStub):
10 | blob = base64.b64decode(
11 | "UklGRmh2AABXRUJQVlA4IFx2AADSvgGd"
12 | ) # Minimal blob with webp magic number.
13 | b64encoded_blob = base64.b64encode(blob).decode("ascii")
14 | with patch.object(
15 | s3_service_stub, "write", wraps=s3_service_stub.write
16 | ) as write_spy:
17 | with patch.object(
18 | s3_service_stub,
19 | "generate_file_url",
20 | wraps=s3_service_stub.generate_file_url,
21 | ) as generate_file_url_spy:
22 | response = ImageHandler.lambda_handler(
23 | event={
24 | "isBase64Encoded": True,
25 | "body": b64encoded_blob,
26 | },
27 | s3=s3_service_stub,
28 | )
29 |
30 | write_spy.assert_called_once_with(
31 | "community-fridge-map-images", "image/webp", blob
32 | )
33 | generate_file_url_spy.assert_called_once()
34 | assert_response(
35 | response,
36 | status=200,
37 | headers={
38 | "Content-Type": "application/json",
39 | "Access-Control-Allow-Origin": "*",
40 | },
41 | body={"photoUrl": ANY},
42 | )
43 |
44 |
45 | def test_upload_invalid_binary(s3_service_stub: S3ServiceStub):
46 | blob = b"notwebp"
47 | b64encoded_blob = base64.b64encode(blob).decode("ascii")
48 | response = ImageHandler.lambda_handler(
49 | event={
50 | "isBase64Encoded": True,
51 | "body": b64encoded_blob,
52 | },
53 | s3=s3_service_stub,
54 | )
55 |
56 | assert_response(
57 | response,
58 | status=400,
59 | headers={
60 | "Content-Type": "application/json",
61 | "Access-Control-Allow-Origin": "*",
62 | },
63 | body={
64 | "message": "Invalid Image Format"
65 | },
66 | )
67 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_layer.py:
--------------------------------------------------------------------------------
1 | from multiprocessing import connection
2 |
3 | from dependencies.python.db import layer_test
4 | from dependencies.python.db import get_ddb_connection
5 |
6 |
7 | def test_layer_test():
8 |
9 | ret = layer_test()
10 | assert ret == "hello world"
11 |
12 |
13 | def test_get_ddb_connection():
14 | connection = get_ddb_connection()
15 | assert str(type(connection)) == ""
16 | connection = get_ddb_connection(env="local")
17 | assert str(type(connection)) == ""
18 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_post_fridge_handler.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | import pytest
4 |
5 | from CommunityFridgeMapApi.functions.fridges.v1.app import FridgeHandler
6 | import json
7 |
8 |
9 | class DynamoDbMockPutItem:
10 | def __init__(self):
11 | pass
12 |
13 | def put_item(self, TableName=None, Item=None, ConditionExpression=None):
14 | pass
15 |
16 | def get_item(self, TableName=None, Key=None):
17 | pass
18 |
19 |
20 | class PostFridgeHandlerTest(unittest.TestCase):
21 | def test_lambda_handler_success(self):
22 | event = {
23 | "body": '{"name": "greenpointfridge", "location": {"address":"9 W. Elm St.", "geoLat": "40.730610", "geoLng": "-73.935242"}}',
24 | "httpMethod": "POST",
25 | "pathParameters": {},
26 | "queryStringParameters": {},
27 | }
28 | response = FridgeHandler.lambda_handler(
29 | event=event, ddbclient=DynamoDbMockPutItem()
30 | )
31 | body = json.loads(response["body"])
32 | self.assertEqual(body, {"id": "greenpointfridge"})
33 | self.assertEqual(response["statusCode"], 201)
34 |
35 | def test_lambda_handler_fail(self):
36 | event = {
37 | "body": '{"name": "greenpointfridge"}',
38 | "httpMethod": "POST",
39 | "pathParameters": {},
40 | "queryStringParameters": {},
41 | }
42 | response = FridgeHandler.lambda_handler(
43 | event=event, ddbclient=DynamoDbMockPutItem()
44 | )
45 | message = json.loads(response["body"])["message"]
46 | self.assertEqual(message, "Missing Required Field: location")
47 | self.assertEqual(response["statusCode"], 400)
48 |
49 | def test_lambda_handler_exception(self):
50 | event = {
51 | "body": '{"name": "greenpointfridge", "location": {"address":"9 W. Elm St.", "geoLat": "40.730610", "geoLng": "-73.935242"}}',
52 | "pathParameters": {},
53 | "queryStringParameters": {},
54 | }
55 | response = FridgeHandler.lambda_handler(
56 | event=event, ddbclient=DynamoDbMockPutItem()
57 | )
58 | self.assertEqual(response["statusCode"], 400)
59 | message = json.loads(response["body"])["message"]
60 | self.assertEqual(message, "httpMethod missing")
61 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_s3_service.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from dependencies.python.s3_service import translate_s3_url_for_client
3 |
4 |
5 | @pytest.mark.parametrize(
6 | "url,env,expected",
7 | [
8 | (
9 | "https://fridge-report.s3.amazonaws.com/wonderful.webp",
10 | "aws",
11 | "https://fridge-report.s3.amazonaws.com/wonderful.webp",
12 | ),
13 | (
14 | "http://localstack:4566/brooklyn.webp",
15 | "local",
16 | "http://localhost:4566/brooklyn.webp",
17 | ),
18 | ]
19 | )
20 | def test_translate_s3_url_for_client(url, env, expected):
21 | actual = translate_s3_url_for_client(url, env=env)
22 | assert actual == expected
23 |
--------------------------------------------------------------------------------
/CommunityFridgeMapApi/tests/unit/test_tag.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from dependencies.python.db import Tag
3 |
4 |
5 | class DynamoDbMockPutItem:
6 | def __init__(self):
7 | pass
8 |
9 | def put_item(self, TableName=None, Item=None, ConditionExpression=None):
10 | pass
11 |
12 |
13 | class TagTest(unittest.TestCase):
14 | def test_is_valid_tag_name(self):
15 | self.assertTrue(Tag.is_valid_tag_name("test-tag")[0])
16 | self.assertEqual(Tag.is_valid_tag_name("test-tag")[1], "")
17 | expected_false = [
18 | Tag.is_valid_tag_name("")[0],
19 | Tag.is_valid_tag_name(None)[0],
20 | Tag.is_valid_tag_name("ab")[0],
21 | Tag.is_valid_tag_name("test&tag")[0],
22 | Tag.is_valid_tag_name("TEST-tag_tag123xyui1234567890__--")[0],
23 | ]
24 | expected_equal = [
25 | (
26 | Tag.is_valid_tag_name("")[1],
27 | "Length of tag_name is 0. It should be >= 3 but <= 32.",
28 | ),
29 | (Tag.is_valid_tag_name(None)[1], "Missing required fields: tag_name"),
30 | (
31 | Tag.is_valid_tag_name("ab")[1],
32 | "Length of tag_name is 2. It should be >= 3 but <= 32.",
33 | ),
34 | (
35 | Tag.is_valid_tag_name("test&tag")[1],
36 | "tag_name contains invalid characters",
37 | ),
38 | (
39 | Tag.is_valid_tag_name("TEST-tag_tag123xyui1234567890__--")[1],
40 | "Length of tag_name is 33. It should be >= 3 but <= 32.",
41 | ),
42 | ]
43 | for i in range(5):
44 | self.assertFalse(expected_false[i])
45 | self.assertEqual(expected_equal[i][0], expected_equal[i][1])
46 |
47 | def test_format_tag(self):
48 | tag = Tag(tag_name="Test Tag", db_client=None)
49 | tag.format_tag("Test Tag")
50 | self.assertEqual(tag.tag_name, "testtag")
51 |
52 | def test_has_required_fields(self):
53 | tag = Tag(tag_name=None, db_client=None)
54 | has_required_field, field = tag.has_required_fields()
55 | self.assertEqual(field, "tag_name")
56 | self.assertFalse(has_required_field)
57 | tag = Tag(tag_name="test tag", db_client=None)
58 | self.assertTrue(tag.has_required_fields())
59 |
60 | def test_add_item(self):
61 | db_client = DynamoDbMockPutItem()
62 | tag = Tag(tag_name="Test_123-Tag", db_client=db_client)
63 | response = tag.add_item()
64 | self.assertTrue(response.success)
65 | self.assertEqual(response.message, "Tag was succesfully added")
66 | self.assertEqual(response.status_code, 200)
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Collective Focus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CFM_Backend
2 |
3 |
4 |
5 |
6 |
7 |
Community Fridge Map
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | A community fridge is a decentralized resource where businesses and individuals can [donate perishable food](https://www.thrillist.com/lifestyle/new-york/nyc-community-fridges-how-to-support). There are dozens of fridges hosted by volunteers across the country.
25 |
26 | Fridge Finder is project sponsored by [Collective Focus](https://collectivefocus.site/), a community organization in Brooklyn, New York. Our goal is to make it easy for people to find fridge locations and get involved with food donation programs in their community. We are building a responsive, mobile first, multi-lingual web application with administrative controls for fridge maintainers. To join the project read our [contributing guidelines](https://github.com/CollectiveFocus/CFM_Frontend/blob/dev/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/FridgeFinder/CFM_Backend/blob/dev/CODE_OF_CONDUCT.md). The application will be deployed to https://www.fridgefinder.app/
27 |
28 | ---
29 | ## Pre-Requisites
30 |
31 | 1. AWS CLI - [Install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
32 | * **You DO NOT have to create an AWS account to use AWS CLI for this project, skip these steps if you don't want to create an AWS account**
33 | * AWS CLI looks for credentials when using it, but doesn't validate. So will need to set some fake one. But the region name matters, use any valid region name.
34 | ```sh
35 | $ aws configure
36 | $ AWS Access Key ID: [ANYTHING YOU WANT]
37 | $ AWS Secret Access Key: [ANYTHING YOUR HEART DESIRES]
38 | $ Default region nam: us-east-1
39 | $ Default output format [None]: (YOU CAN SKIP)
40 | ```
41 | 2. SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
42 | * **You DO NOT need to create an aws account to use SAM CLI for this project, skip these steps if you don't want to create an aws account**
43 | 3. Python 3 - [Install Python 3](https://www.python.org/downloads/)
44 | 4. Docker - [Install Docker](https://docs.docker.com/get-docker/)
45 |
46 | ---
47 |
48 | ## Setup Local Database Connection
49 |
50 | **Guide that was used:** https://betterprogramming.pub/how-to-deploy-a-local-serverless-application-with-aws-sam-b7b314c3048c
51 |
52 | Follow these steps to get Dynamodb running locally
53 |
54 | 1. **Start a local DynamoDB service**
55 | ```sh
56 | $ docker compose up
57 | # OR if you want to run it in the background:
58 | $ docker compose up -d
59 | ```
60 | 2. **Create tables**
61 | ```sh
62 | $ ./scripts/create_local_dynamodb_tables.py
63 | ```
64 | 3. `cd CommunityFridgeMapApi/`
65 | 4. `sam build --use-container`
66 | 5. **Load data into your local Dynamodb tables**
67 | 1. Fridge Data: `sam local invoke LoadFridgeDataFunction --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
68 | 6. **Get data from your local Dynamodb tables**
69 | 1. `aws dynamodb scan --table-name fridge_dev --endpoint-url http://localhost:4566`
70 | ---
71 | ## Build and Test Locally
72 |
73 | Confirm that the following requests work for you
74 |
75 | 1. `cd CommunityFridgeMapApi/`
76 | 2. ` sam build --use-container`
77 | 3. `sam local invoke HelloWorldFunction --event events/event.json`
78 | * response: ```{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}```
79 | 4. `sam local start-api`
80 | 5. `curl http://localhost:3000/hello`
81 | * response: ```{"message": "hello world"}```
82 | ---
83 | ## API
84 |
85 | Choose your favorite API platform for using APIs.
86 | Recommend: https://www.postman.com/
87 |
88 |
89 | ### Fridge
90 |
91 | ### One Time Use
92 | 1. POST Fridge: `sam local invoke FridgesFunction --event events/local-post-fridge-event.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
93 | 2. GET Fridge: `sam local invoke FridgesFunction --event events/local-event-get-fridge.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
94 | 3. GET Fridges: `sam local invoke FridgesFunction --event events/local-event-get-fridges.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
95 | 4. GET Fridges Filter By Tag: `sam local invoke FridgesFunction --event events/local-event-get-fridges-with-tag.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
96 |
97 | ### Local Server
98 | 1. Start Server: `sam local start-api --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
99 | 2. GET Fridge: Go to http://localhost:3000/v1/fridges/{fridgeId}
100 | * Example: http://localhost:3000/v1/fridges/thefriendlyfridge
101 | 3. GET Fridges: Go to http://localhost:3000/v1/fridges
102 | 4. Get Fridges Filter By Tag: http://localhost:3000/v1/fridges?tag={TAG}
103 | * Example: http://localhost:3000/v1/fridges?tag=tag1
104 | 5. POST Fridge Example:
105 | ```
106 | curl --location --request POST 'http://127.0.0.1:3000/v1/fridges' --header 'Content-Type: application/json' --data-raw '{
107 | "name": "LES Community Fridge #2",
108 | "verified": false,
109 | "location": {
110 | "name": "testing",
111 | "street": "466 Grand Street",
112 | "city": "New York",
113 | "state": "NY",
114 | "zip": "10002",
115 | "geoLat": 40.715207,
116 | "geoLng": -73.983748
117 | },
118 | "maintainer": {
119 | "name": "name",
120 | "organization": "org",
121 | "phone": "1234567890",
122 | "email": "test@test.com",
123 | "instagram": "https://www.instagram.com/les_communityfridge",
124 | "website": "https://linktr.ee/lescommunityfridge"
125 | },
126 | "notes": "notes",
127 | "photoUrl": "url.com"
128 | }'
129 | ```
130 | ### Fridge Report
131 |
132 | #### One Time Use
133 | 1. POST FridgeReport: `sam local invoke FridgeReportFunction --event events/local-fridge-report-event.json --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
134 | * [OPTIONAL] Generate custom event Example: `sam local generate-event apigateway aws-proxy --method POST --path document --body "{\"condition\": \"working\", \"foodPercentage\": 0}" > events/local-fridge-report-event-2.json`
135 | * Add `"fridgeId": "{FRIDGEID}"` to pathParameter in generated file
136 | 2. Query Data: `aws dynamodb scan --table-name fridge_report_dev --endpoint-url http://localhost:8000`
137 |
138 | #### Local Server
139 | 1. Start Server: `sam local start-api --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
140 | 2. Make a POST Request to: `http://127.0.0.1:3000/v1/fridges/{fridgeId}/reports`
141 | * Example: `curl --location --request POST 'http://127.0.0.1:3000/v1/fridges/thefriendlyfridge/reports' --header 'Content-Type: application/json' --data-raw '{"condition": "good", "foodPercentage": 1}'`
142 |
143 | ### Image
144 |
145 | 1. Start local SAM API `sam local start-api --parameter-overrides ParameterKey=Environment,ParameterValue=local ParameterKey=Stage,ParameterValue=dev --docker-network cfm-network`
146 | 1. Upload image (replace `` with your actual image path like `"@/home/user/Downloads/sample.webp"`)
147 | ```
148 | curl --request POST \
149 | --url http://localhost:3000/v1/photo \
150 | --header 'Content-Type: image/webp' \
151 | --data-binary "@"
152 | ```
153 | ---
154 | ## Tests
155 |
156 | Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests.
157 |
158 | ```bash
159 | CFM_BACKEND$ cd CommunityFridgeMapApi
160 | CommunityFridgeMapApi$ pip install -r tests/requirements.txt --user
161 | # unit test
162 | CommunityFridgeMapApi$ python -m pytest tests/unit -v
163 | ```
164 |
165 | To test with coverage
166 | ```bash
167 | CommunityFridgeMapApi$ coverage run -m pytest tests/unit -v
168 | CommunityFridgeMapApi$ coverage report
169 | CommunityFridgeMapApi$ coverage html
170 | ```
171 |
172 | MacOS:
173 | ```bash
174 | CommunityFridgeMapApi$ open -a "Google Chrome" htmlcov/index.html
175 | ```
176 |
177 | Windows:
178 | ```bash
179 | CommunityFridgeMapApi$ start "Google Chrome" htmlcov/index.html
180 | ```
181 | ---
182 | ## Useful AWS SAM commands
183 | 1. `sam validate -t template.yaml`
184 | 2. `sam build --use-container`
185 | * Use this command before running the backend if you updated the code
186 | 3. `sam local generate-event apigateway aws-proxy --method GET --path document --body "" > local-event.json`
187 | * Use this command to generate a REST API event
188 |
189 | ## Useful Dynamodb Commands
190 | 1. `aws dynamodb scan --table-name fridge_{stage} --endpoint-url http://localhost:4566`
191 | 2. `aws dynamodb scan --table-name fridge_report_{stage} --endpoint-url http://localhost:4566`
192 |
193 | ## Useful formatting Command
194 | ```bash
195 | CFM_BACKEND$ bash .git/hooks/pre-commit
196 | ```
197 |
198 | ## Resources
199 |
200 | Project Documentation
201 |
202 | - [Architecture Brainstorming](https://docs.google.com/document/d/1FYClUD16KUY42_p93rZFHN-iyp94RU0Rtw517vj2jXs/edit)
203 | - [Architecture](https://docs.google.com/document/d/1yZVGAxVn4CEZyyce_Zuha3oYOOU8ey7ArBvLbm7l4bw/edit)
204 | - [Database Tables] (https://docs.google.com/document/d/16hjNHxm_ebZv8u_VolT1bdlJEDccqb7V67-Y6ljjUdY/edit?usp=sharing)
205 | - [Development Workflow](https://docs.google.com/document/d/1m9Xqo4QUVEBjMD7sMjxSHa3CxxjvrHppwc0nrdWCAAc/edit)
206 | - [Database Table Design](https://docs.google.com/document/d/16hjNHxm_ebZv8u_VolT1bdlJEDccqb7V67-Y6ljjUdY/edit?usp=sharing)
207 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | localstack:
5 | image: localstack/localstack:latest
6 | ports:
7 | - "4566:4566" # LocalStack Gateway
8 | - "4510-4559:4510-4559" # external services port range
9 | environment:
10 | - DEBUG=${DEBUG-}
11 | - PERSISTENCE=${PERSISTENCE-}
12 | - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
13 | - DOCKER_HOST=unix:///var/run/docker.sock
14 | volumes:
15 | - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
16 | - "/var/run/docker.sock:/var/run/docker.sock"
17 |
18 | networks:
19 | default:
20 | name: cfm-network
21 | driver: bridge
22 |
23 |
--------------------------------------------------------------------------------
/schema/fridge.json:
--------------------------------------------------------------------------------
1 | {
2 | "TableName": "fridge_dev",
3 | "AttributeDefinitions": [
4 | {
5 | "AttributeName": "id",
6 | "AttributeType": "S"
7 | }
8 | ],
9 | "KeySchema": [
10 | {
11 | "AttributeName": "id",
12 | "KeyType": "HASH"
13 | }
14 | ],
15 | "ProvisionedThroughput": {
16 | "ReadCapacityUnits": 5,
17 | "WriteCapacityUnits": 5
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/schema/fridge_history.json:
--------------------------------------------------------------------------------
1 | {
2 | "TableName": "fridge_history_dev",
3 | "AttributeDefinitions": [
4 | {
5 | "AttributeName": "fridgeId",
6 | "AttributeType": "S"
7 | },
8 | {
9 | "AttributeName": "epochTimestamp",
10 | "AttributeType": "N"
11 | }
12 | ],
13 | "KeySchema": [
14 | {
15 | "AttributeName": "fridgeId",
16 | "KeyType": "HASH"
17 | },
18 | {
19 | "AttributeName": "epochTimestamp",
20 | "KeyType": "RANGE"
21 | }
22 | ],
23 | "ProvisionedThroughput": {
24 | "ReadCapacityUnits": 5,
25 | "WriteCapacityUnits": 5
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/schema/fridge_report.json:
--------------------------------------------------------------------------------
1 | {
2 | "TableName": "fridge_report_dev",
3 | "AttributeDefinitions": [
4 | {
5 | "AttributeName": "fridgeId",
6 | "AttributeType": "S"
7 | },
8 | {
9 | "AttributeName": "epochTimestamp",
10 | "AttributeType": "N"
11 | }
12 | ],
13 | "KeySchema": [
14 | {
15 | "AttributeName": "fridgeId",
16 | "KeyType": "HASH"
17 | },
18 | {
19 | "AttributeName": "epochTimestamp",
20 | "KeyType": "RANGE"
21 | }
22 | ],
23 | "ProvisionedThroughput": {
24 | "ReadCapacityUnits": 5,
25 | "WriteCapacityUnits": 5
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/schema/tag.json:
--------------------------------------------------------------------------------
1 | {
2 | "TableName": "tag_dev",
3 | "AttributeDefinitions": [
4 | {
5 | "AttributeName": "tag_name",
6 | "AttributeType": "S"
7 | }
8 | ],
9 | "KeySchema": [
10 | {
11 | "AttributeName": "tag_name",
12 | "KeyType": "HASH"
13 | }
14 | ],
15 | "ProvisionedThroughput": {
16 | "ReadCapacityUnits": 5,
17 | "WriteCapacityUnits": 5
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/create_local_dynamodb_tables.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import subprocess
4 | from pathlib import Path
5 |
6 | ENDPOINT_URL = "http://localhost:4566"
7 |
8 | scripts_dir = os.path.dirname(__file__)
9 | schema_dir = os.path.join(scripts_dir, "../schema")
10 |
11 | for file in os.listdir(schema_dir):
12 | file_path = os.path.abspath(os.path.join(schema_dir, file))
13 | if file.endswith(".json"):
14 | p = subprocess.run(
15 | [
16 | "aws",
17 | "dynamodb",
18 | "create-table",
19 | "--cli-input-json",
20 | Path(file_path).as_uri(),
21 | "--endpoint-url",
22 | ENDPOINT_URL,
23 | ],
24 | env=dict(os.environ, AWS_PAGER=""),
25 | )
26 | if p.returncode:
27 | print(f"Could not create DynamoDB table from {file}")
28 | exit(p.returncode)
29 |
30 |
--------------------------------------------------------------------------------