├── preview.png
├── rigo-baby.jpeg
├── .vscode
└── settings.json
├── .gitignore
├── .github
└── workflows
│ └── validate-integrity.yml
├── .gitpod.yml
├── Pipfile
├── .gitpod.Dockerfile
├── .devcontainer
└── devcontainer.json
├── src
├── app.py
├── datastructures.py
├── utils.py
└── test_solution.py
├── learn.json
├── README.md
├── README.es.md
└── Pipfile.lock
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/breatheco-de/exercise-family-static-api/HEAD/preview.png
--------------------------------------------------------------------------------
/rigo-baby.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/breatheco-de/exercise-family-static-api/HEAD/rigo-baby.jpeg
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.editorAssociations": {
3 | "*.md": "vscode.markdown.preview.editor",
4 | }
5 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .venv
3 | database.database
4 | database.db
5 | diagram.png
6 | public/*
7 | dist/*
8 | package.json
9 |
10 | __pycache__/
--------------------------------------------------------------------------------
/.github/workflows/validate-integrity.yml:
--------------------------------------------------------------------------------
1 | name: Validate Integrity
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v3
11 | - uses: actions/setup-node@v3
12 | with:
13 | node-version: '14.x'
14 | - run: npm install @learnpack/learnpack -g
15 | - run: learnpack audit
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | image:
2 | file: .gitpod.Dockerfile
3 | ports:
4 | - port: 3000
5 | onOpen: open-preview
6 | visibility: public
7 | tasks:
8 | - init: >
9 | pipenv install
10 | python docs/assets/welcome.py
11 | command: >
12 | pipenv run start;
13 | github:
14 | prebuilds:
15 | # enable for the master/default branch (defaults to true)
16 | main: true
17 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 |
8 | [packages]
9 | flask = "*"
10 | psycopg2-binary = "*"
11 | flask-cors = "*"
12 | pytest = "*"
13 | mock = "*"
14 | pytest-testdox = "*"
15 | pytest-ordering = "*"
16 |
17 | [requires]
18 | python_version = "3.13"
19 |
20 | [scripts]
21 | start="python src/app.py"
22 | test="pytest --testdox --color=yes"
23 |
--------------------------------------------------------------------------------
/.gitpod.Dockerfile:
--------------------------------------------------------------------------------
1 |
2 | FROM gitpod/workspace-postgres:latest
3 |
4 | SHELL ["/bin/bash", "-c"]
5 |
6 | RUN sudo apt-get update \
7 | && sudo apt-get update \
8 | && sudo apt-get clean \
9 | && sudo rm -rf /var/cache/apt/* /var/lib/apt/lists/* /tmp/*
10 |
11 | # That Gitpod install pyenv for me? no, thanks
12 | WORKDIR /home/gitpod/
13 | RUN rm .pyenv -Rf
14 | RUN rm .gp_pyenv.d -Rf
15 | RUN curl https://pyenv.run | bash
16 |
17 |
18 | RUN pyenv update && pyenv install 3.10.7 && pyenv global 3.10.7
19 | RUN pip install pipenv
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python
3 | {
4 | "name": "Python 3",
5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6 | "image": "mcr.microsoft.com/devcontainers/python:3.13",
7 |
8 | // Features to add to the dev container. More info: https://containers.dev/features.
9 | // "features": {},
10 |
11 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
12 | // "forwardPorts": [],
13 |
14 | // Use 'postCreateCommand' to run commands after the container is created.
15 | "onCreateCommand": "pipenv install",
16 |
17 | // Configure tool-specific properties.
18 | // "customizations": {},
19 |
20 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
21 | // "remoteUser": "root"
22 | }
23 |
--------------------------------------------------------------------------------
/src/app.py:
--------------------------------------------------------------------------------
1 | """
2 | This module takes care of starting the API Server, Loading the DB and Adding the endpoints
3 | """
4 | import os
5 | from flask import Flask, request, jsonify, url_for
6 | from flask_cors import CORS
7 | from utils import APIException, generate_sitemap
8 | from datastructures import FamilyStructure
9 | # from models import Person
10 |
11 |
12 | app = Flask(__name__)
13 | app.url_map.strict_slashes = False
14 | CORS(app)
15 |
16 | # Create the jackson family object
17 | jackson_family = FamilyStructure("Jackson")
18 |
19 |
20 | # Handle/serialize errors like a JSON object
21 | @app.errorhandler(APIException)
22 | def handle_invalid_usage(error):
23 | return jsonify(error.to_dict()), error.status_code
24 |
25 |
26 | # Generate sitemap with all your endpoints
27 | @app.route('/')
28 | def sitemap():
29 | return generate_sitemap(app)
30 |
31 |
32 | @app.route('/members', methods=['GET'])
33 | def handle_hello():
34 | # This is how you can use the Family datastructure by calling its methods
35 | members = jackson_family.get_all_members()
36 | response_body = {"hello": "world",
37 | "family": members}
38 | return jsonify(response_body), 200
39 |
40 |
41 |
42 | # This only runs if `$ python src/app.py` is executed
43 | if __name__ == '__main__':
44 | PORT = int(os.environ.get('PORT', 3000))
45 | app.run(host='0.0.0.0', port=PORT, debug=True)
46 |
--------------------------------------------------------------------------------
/src/datastructures.py:
--------------------------------------------------------------------------------
1 | """
2 | Update this file to implement the following already declared methods:
3 | - add_member: Should add a member to the self._members list
4 | - delete_member: Should delete a member from the self._members list
5 | - get_member: Should return a member from the self._members list
6 | """
7 |
8 | class FamilyStructure:
9 | def __init__(self, last_name):
10 | self.last_name = last_name
11 | self._next_id = 1
12 | self._members = [
13 | {
14 | "id": self._generate_id(),
15 | "first_name": "John",
16 | "last_name": last_name,
17 | "age": 33,
18 | "lucky_numbers": [7, 13, 22]
19 | }
20 | ]
21 |
22 | # This method generates a unique incremental ID
23 | def _generate_id(self):
24 | generated_id = self._next_id
25 | self._next_id += 1
26 | return generated_id
27 |
28 | def add_member(self, member):
29 | ## You have to implement this method
30 | ## Append the member to the list of _members
31 | pass
32 |
33 | def delete_member(self, id):
34 | ## You have to implement this method
35 | ## Loop the list and delete the member with the given id
36 | pass
37 |
38 | def get_member(self, id):
39 | ## You have to implement this method
40 | ## Loop all the members and return the one with the given id
41 | pass
42 |
43 | # This method is done, it returns a list with all the family members
44 | def get_all_members(self):
45 | return self._members
--------------------------------------------------------------------------------
/learn.json:
--------------------------------------------------------------------------------
1 | {
2 | "gitpod": true,
3 | "title" : {
4 | "us": "Family Static API with Flask",
5 | "es": "API Estática de Familia con Flask"
6 | },
7 | "slug" : "family-static-api",
8 | "status": "published",
9 | "translations": ["es", "us"],
10 | "preview": "https://github.com/breatheco-de/exercise-family-static-api/blob/master/preview.png?raw=true",
11 | "solution": "https://github.com/breatheco-de/exercise-family-static-api/tree/solution",
12 | "difficulty": "beginner",
13 | "technologies": ["flask", "python", "json", "serialization", "data-structures", "rest","api"],
14 | "duration" : 8,
15 | "template_url": "self",
16 | "description" : {
17 | "us": "Build a family management API with Flask, focusing on creating efficient data structures to handle and manipulate family member information. In this project, you'll learn how to develop dynamic RESTful API endpoints, manage in-memory data, and handle JSON responses. Gain hands-on experience in Flask, data management, and API design",
18 | "es": "Crea una API de gestión de familiares con Flask, enfocándote en desarrollar estructuras de datos eficientes para manejar y manipular la información de los miembros de la familia. En este proyecto, aprenderás a desarrollar puntos finales dinámicos de API RESTful, gestionar datos en memoria y manejar respuestas en formato JSON. Obtén experiencia práctica en Flask, gestión de datos y diseño de APIs."
19 | },
20 | "talents": [
21 | { "badge": "identator", "points": 10 }
22 | ],
23 | "autoPlay": false,
24 | "projectType": "project",
25 | "telemetry": {
26 | "batch": "https://breathecode.herokuapp.com/v1/assignment/me/telemetry?asset_id=188"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils.py:
--------------------------------------------------------------------------------
1 | from flask import jsonify, url_for
2 |
3 |
4 | class APIException(Exception):
5 | status_code = 400
6 |
7 | def __init__(self, message, status_code=None, payload=None):
8 | Exception.__init__(self)
9 | self.message = message
10 | if status_code is not None:
11 | self.status_code = status_code
12 | self.payload = payload
13 |
14 | def to_dict(self):
15 | rv = dict(self.payload or ())
16 | rv['message'] = self.message
17 | return rv
18 |
19 |
20 | def has_no_empty_params(rule):
21 | defaults = rule.defaults if rule.defaults is not None else ()
22 | arguments = rule.arguments if rule.arguments is not None else ()
23 | return len(defaults) >= len(arguments)
24 |
25 |
26 | def generate_sitemap(app):
27 | links = []
28 | for rule in app.url_map.iter_rules():
29 | # Filter out rules we can't navigate to in a browser
30 | # and rules that require parameters
31 | if "GET" in rule.methods and has_no_empty_params(rule):
32 | url = url_for(rule.endpoint, **(rule.defaults or {}))
33 | links.append(url)
34 | links_html = "".join(["
" + y + "" for y in links])
35 | return """
36 |
37 |

38 |
Hello Rigo!!
39 | This is your api home, remember to specify a real endpoint path like:
40 |
41 | """ + links_html + """
42 |
43 |
44 | """
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Family Static API
3 |
4 |
5 | The Jackson Family needs a static API! We need to build the *data structures* and create the API endpoints to interact with it using [Hoppscotch](https://hoppscotch.io/) (recommended) or Postman.
6 |
7 |
8 |
9 | ## 🌱 How to start this project
10 |
11 | This project comes with the necessary files to start working immediately.
12 |
13 | We recommend opening this very same repository using a provisioning tool like [Codespaces](https://4geeks.com/lesson/what-is-github-codespaces) (recommended) or [Gitpod](https://4geeks.com/lesson/how-to-use-gitpod). Alternatively, you can clone it on your local computer using the `git clone` command.
14 |
15 | This is the repository you need to open:
16 |
17 | ```txt
18 | https://github.com/breatheco-de/exercise-family-static-api
19 | ```
20 |
21 | **👉 Please follow these steps on** [how to start a coding project](https://4geeks.com/lesson/how-to-start-a-project).
22 |
23 |
24 |
25 | ## 💻 Installation
26 |
27 | 1. Install the project dependencies by running `$ pipenv install`
28 |
29 | 2. Get inside the virtual environment by running `$ pipenv shell`
30 |
31 | 3. Start the server by running `$ pipenv run start`
32 |
33 | ## ✅ Automatic grading
34 |
35 | + Test your code by running `$ pipenv run test`
36 |
37 | ## 📝 Instructions
38 |
39 | 1. Create the code needed to implement the API endpoints described further below.
40 |
41 | 2. The only two files you have to edit are:
42 |
43 | - `src/datastructure.py`: Contains the class with the rules on how to manage the family members.
44 | - `src/app.py`: Contains the API, it uses the Family as data structure.
45 |
46 | 3. We have prepared a set of automated tests that will give you an idea if your code is correct. Run the tests by typing `$ pipenv run test` on the command line.
47 |
48 | ## Data structures
49 |
50 | Every **member** of the Jackson family must be a dictionary - the equivalent of [Objects Literals in JS](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects) - and have these values:
51 |
52 | ```python
53 | + id: Int
54 | + first_name: String
55 | + last_name: String (Always Jackson)
56 | + age: Int > 0
57 | + lucky_numbers: List of integers
58 | ```
59 |
60 | The **family** data-structure will be a class with the following structure:
61 |
62 | ```python
63 | class FamilyStructure:
64 | def __init__(self, last_name):
65 | self.last_name = last_name
66 | self._next_id = 1
67 | self._members = []
68 |
69 | # This method generates a unique 'id' when adding members into the list (you shouldn't touch this function)
70 | def _generate_id(self):
71 | generated_id = self._next_id
72 | self._next_id += 1
73 | return generated_id
74 |
75 | def add_member(self, member):
76 | ## You have to implement this method
77 | ## Append the member to the list of _members
78 | pass
79 |
80 | def delete_member(self, id):
81 | ## You have to implement this method
82 | ## Loop the list and delete the member with the given id
83 | pass
84 |
85 | def get_member(self, id):
86 | ## You have to implement this method
87 | ## Loop all the members and return the one with the given id
88 | pass
89 |
90 | def get_all_members(self):
91 | return self._members
92 | ```
93 |
94 | Note: don't forget to initialize the class: `jackson_family = FamilyStructure('Jackson')` *before* the routes.
95 |
96 | ## These are the initial Family Members
97 |
98 | ```md
99 | John Jackson
100 | 33 Years old
101 | Lucky Numbers: 7, 13, 22
102 |
103 | Jane Jackson
104 | 35 Years old
105 | Lucky Numbers: 10, 14, 3
106 |
107 | Jimmy Jackson
108 | 5 Years old
109 | Lucky Numbers: 1
110 | ```
111 |
112 | ## Endpoints
113 |
114 | This API must have 4 endpoints. They all return JSON:
115 |
116 | ### 1) Get all family members:
117 |
118 | Which returns all members of the family.
119 |
120 | ```md
121 | GET /members
122 |
123 | status_code: 200 if success. 400 if bad request (wrong info). 500 if the server encounters an error
124 |
125 | RESPONSE BODY (content-type: application/json):
126 |
127 | []
128 | ```
129 |
130 | ### 2) Retrieve one member
131 |
132 | Which returns the member of the family where `id == member_id`.
133 |
134 | ```md
135 | GET /members/
136 |
137 | RESPONSE (content_type: application/json):
138 |
139 | status_code: 200 if success. 400 if bad request (wrong info). 500 if the server encounters an error
140 |
141 |
142 | body:
143 | {
144 | "id": Int,
145 | "first_name": String,
146 | "age": Int,
147 | "lucky_numbers": List
148 | }
149 |
150 | ```
151 |
152 | ### 3) Add (POST) new member
153 |
154 | Which adds a new member to the family data structure.
155 |
156 | ```md
157 | POST /members
158 |
159 | REQUEST BODY (content_type: application/json):
160 | {
161 | id: Int,
162 | first_name: String,
163 | age: Int,
164 | lucky_numbers: []
165 | }
166 |
167 | RESPONSE (content_type: application/json):
168 |
169 | status_code: 200 if success. 400 if a bad request (wrong info). 500 if the server encounters an error
170 | ```
171 |
172 | ### 4) DELETE one member
173 |
174 | Which deletes a family member with `id == member_id`
175 |
176 | ```md
177 | DELETE /members/
178 |
179 | RESPONSE (content_type: application/json):
180 |
181 | status_code: 200 if success. 400 if a bad request (wrong info). 500 if the server encounters an error
182 |
183 | body: {
184 | done: True
185 | }
186 | ```
187 |
188 | ## Requirements
189 |
190 | - All requests and responses should be in content/type: application/json
191 | - Response codes must be `200` for success, `400` for bad request, or `404` for not found.
192 | - These exercises do not include a database, everything must be done in Runtime (RAM).
193 |
194 | This and many other projects are built by students as part of the 4Geeks Academy [Coding Bootcamp](https://4geeksacademy.com/us/coding-bootcamp) by [Alejandro Sanchez](https://twitter.com/alesanchezr) and many other contributors. Find out more about our [Full Stack Developer Course](https://4geeksacademy.com/us/coding-bootcamps/part-time-full-stack-developer), and [Data Science Bootcamp](https://4geeksacademy.com/us/coding-bootcamps/datascience-machine-learning).
195 |
--------------------------------------------------------------------------------
/src/test_solution.py:
--------------------------------------------------------------------------------
1 | import pytest, os, sys, tempfile, mock, json
2 | from flask import Flask
3 |
4 |
5 | @pytest.fixture
6 | def client():
7 | with mock.patch('flask.Flask', lambda x: Flask(x)):
8 | from app import app
9 | db_fd, app.config['DATABASE'] = tempfile.mkstemp()
10 | app.config['TESTING'] = True
11 | with app.test_client() as client:
12 | yield client
13 | os.close(db_fd)
14 | os.unlink(app.config['DATABASE'])
15 |
16 |
17 | @pytest.mark.it("The Family structure must be initialized with the 3 members specified in the instructions")
18 | def test_first_three(client):
19 | response = client.get('/members')
20 | members = json.loads(response.data)
21 | assert len(members) == 3, "The Family structure must be initialized with the 3 members specified in the instructions"
22 |
23 |
24 | @pytest.mark.it("Implement the POST /members method to add a new member")
25 | def test_add_implementation(client):
26 | response = client.post('/members', json={
27 | "first_name": "Tommy",
28 | "age": 23,
29 | "lucky_numbers": [34, 65, 23, 4, 6]
30 | })
31 | assert response.status_code == 200, "Implement the POST /members method to add a new member"
32 |
33 |
34 | @pytest.mark.it("The POST /members method should return something, NOT EMPTY")
35 | def test_add_empty_response_body(client):
36 | response = client.post('/members', json={
37 | "first_name": "Sandra",
38 | "age": 12,
39 | "lucky_numbers": [12, 34, 33, 45, 32, 12]
40 | })
41 | assert response.data != b"", "The POST /members method should return something, NOT EMPTY"
42 |
43 |
44 | @pytest.mark.it("Implement the GET /members method")
45 | def test_get_members_exist(client):
46 | response = client.get('/members')
47 | assert response.status_code == 200
48 |
49 |
50 | @pytest.mark.it("The GET /members method should return a list")
51 | def test_get_members_returns_list(client):
52 | response = client.get('/members')
53 | data = json.loads(response.data)
54 | assert isinstance(data, list), "The GET /members method should return a list"
55 |
56 |
57 | @pytest.mark.it("We added two members using POST /members, so calling GET /members should return a list of length == 5")
58 | def test_get_members_returns_list_of_five(client):
59 | response = client.get('/members')
60 | members = json.loads(response.data)
61 | assert len(members) == 5, "We added two members using POST /members, so calling GET /members should return a list of length == 5"
62 |
63 |
64 | @pytest.mark.it("The GET /members/ method should exist")
65 | def test_get_single_member_implemented(client):
66 | post_response = client.post('/members', json={
67 | "first_name": "Tommy",
68 | "age": 23,
69 | "lucky_numbers": [1, 2, 3]
70 | })
71 | tommy = json.loads(post_response.data)
72 | get_response = client.get(f"/members/{tommy['id']}")
73 | assert get_response.status_code == 200, "The GET /members/ method should exist"
74 |
75 |
76 | @pytest.mark.it("The GET /members/ method should return a single family member in dictionary format")
77 | def test_get_single_member_returns_dict(client):
78 | post_response = client.post('/members', json={
79 | "first_name": "Tommy",
80 | "age": 23,
81 | "lucky_numbers": [1, 2, 3]
82 | })
83 | tommy = json.loads(post_response.data)
84 | get_response = client.get(f"/members/{tommy['id']}")
85 | data = json.loads(get_response.data)
86 | assert data is not None, "The GET /members/ method should return a single family member in dictionary format"
87 | assert isinstance(data, dict), "The GET /members/ method should return a single family member in dictionary format"
88 |
89 |
90 | @pytest.mark.it("The dictionary returned by GET /members/ should contain the keys: [first_name, id, age, lucky_numbers]")
91 | def test_get_single_member_has_keys(client):
92 | post_response = client.post('/members', json={
93 | "first_name": "Tommy",
94 | "age": 23,
95 | "lucky_numbers": [1, 2, 3]
96 | })
97 | tommy = json.loads(post_response.data)
98 | response = client.get(f"/members/{tommy['id']}")
99 | data = json.loads(response.data)
100 |
101 | assert data is not None, "The dictionary returned by GET /members/ should contain the keys: [first_name, id, age, lucky_numbers]"
102 | assert "first_name" in data, "The dictionary returned by GET /members/ should contain the keys: [first_name, id, age, lucky_numbers]"
103 | assert "id" in data, "The dictionary returned by GET /members/ should contain the keys: [first_name, id, age, lucky_numbers]"
104 | assert "age" in data
105 | assert "lucky_numbers" in data
106 |
107 |
108 | @pytest.mark.it("The GET /members/ method should return Tommy")
109 | def test_get_first_member_tommy(client):
110 | post_response = client.post('/members', json={
111 | "first_name": "Tommy",
112 | "age": 23,
113 | "lucky_numbers": [1]
114 | })
115 | tommy = json.loads(post_response.data)
116 | response = client.get(f"/members/{tommy['id']}")
117 | data = json.loads(response.data)
118 | assert data["first_name"] == "Tommy", "The GET /members/ method should return Tommy"
119 |
120 |
121 | @pytest.mark.it("Implement the DELETE /members/ method to delete a family member")
122 | def test_delete_member(client):
123 | post_response = client.post('/members', json={
124 | "first_name": "Tommy",
125 | "age": 23,
126 | "lucky_numbers": [1, 2, 3]
127 | })
128 | tommy = json.loads(post_response.data)
129 | delete_response = client.delete(f"/members/{tommy['id']}")
130 | assert delete_response.status_code == 200, "Implement the DELETE /members/ method to delete a family member"
131 |
132 |
133 | @pytest.mark.it("The DELETE /members/ method should return a dictionary with the 'done' key")
134 | def test_delete_response(client):
135 | post_response = client.post('/members', json={
136 | "first_name": "Tommy",
137 | "age": 23,
138 | "lucky_numbers": [1, 2, 3]
139 | })
140 | tommy = json.loads(post_response.data)
141 | delete_response = client.delete(f"/members/{tommy['id']}")
142 | assert delete_response.json["done"] == True, "The DELETE /members/ method should return a dictionary with the 'done' key"
143 |
144 |
--------------------------------------------------------------------------------
/README.es.md:
--------------------------------------------------------------------------------
1 |
2 | # API Estática Familiar
3 |
4 |
5 | ¡La familia "Jackson" necesita una API estática! Necesitamos construir las *estructuras de datos (data structures)* y crear un API endpoint para interactuar con él utilizando [Hoppscotch](https://hoppscotch.io/) (recomendado) o Postman.
6 |
7 |
8 |
9 | ## 🌱 Cómo comenzar este proyecto
10 |
11 | Este proyecto viene con los archivos necesarios para comenzar a trabajar de inmediato.
12 |
13 | Recomendamos abrir este mismo repositorio usando un entorno de desarrollo como [Codespaces](https://4geeks.com/es/lesson/tutorial-de-github-codespaces) (recomendado) o [Gitpod](https://4geeks.com/es/lesson/como-utilizar-gitpod). Alternativamente, puedes clonarlo en tu computadora local usando el comando `git clone`.
14 |
15 | Este es el repositorio que necesitas abrir:
16 |
17 | ```txt
18 | https://github.com/breatheco-de/exercise-family-static-api
19 | ```
20 |
21 |
22 |
23 | ## 💻 Instalación
24 |
25 | 1. Instala las dependencias del proyecto `$ pipenv install`.
26 |
27 | 2. Entra dentro del *virtual environment* `$ pipenv shell`
28 |
29 | 3. Inicia el servidor flask `$ pipenv run start`
30 |
31 | ## ✅ Autoevaluación
32 |
33 | + Evalúa tu código con el comando `$ pipenv run test`
34 |
35 | ## 📝 Instrucciones
36 |
37 | 1. Crea el código necesario para desarrollar los API endpoints descritos más adelante.
38 |
39 | 2. Los únicos dos archivos que tienes que editar son:
40 |
41 | - `src/datastructure.py`: Contiene la estructura de datos `FamilyStructure` que se encarga de manejar la familia.
42 | - `src/app.py`: Es el código de tu API, aquí debes agregar los endpoints (rutas) y la lógica de programación.
43 |
44 | 3. Hemos preparado un conjunto de pruebas automatizadas que te darán una idea de si tu código es correcto, ejecuta las pruebas escribiendo `$ pipenv run test` en la línea de comandos (terminal o consola).
45 |
46 | ## Estructuras de datos (Data structures)
47 |
48 | Cada **miembro** de la familia Jackson debe ser un diccionario, equivalente a [Objetos literales en JS](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects) - y tienen estos valores:
49 |
50 | ```python
51 | + id: Int
52 | + first_name: String
53 | + last_name: String (Siempre Jackson)
54 | + age: Int > 0
55 | + lucky_numbers: List of integers
56 | ```
57 |
58 | La estructura de datos **family** será una clase con la siguiente estructura:
59 |
60 | ```python
61 | class FamilyStructure:
62 | def __init__(self, last_name):
63 | self.last_name = last_name
64 | self._next_id = 1
65 | self._members = []
66 |
67 | # Este método genera un 'id' único al agregar miembros a la lista (no debes modificar esta función)
68 | def _generate_id(self):
69 | generated_id = self._next_id
70 | self._next_id += 1
71 | return generated_id
72 |
73 | def add_member(self, member):
74 | ## Debes implementar este método
75 | ## Agrega un nuevo miembro a la lista de _members
76 | pass
77 |
78 | def delete_member(self, id):
79 | ## Debes implementar este método
80 | ## Recorre la lista y elimina el miembro con el id proporcionado
81 | pass
82 |
83 | def get_member(self, id):
84 | ## Debes implementar este método
85 | ## Recorre la lista y obtén el miembro con el id proporcionado
86 | pass
87 |
88 | def get_all_members(self, id):
89 | return self._members
90 | ```
91 |
92 | Nota: no olvides inicializar la clase: `jackson_family = FamilyStructure('Jackson')` *antes* de las rutas.
93 |
94 | ## Estos son los miembros iniciales de la familia.
95 |
96 | ```md
97 | John Jackson
98 | 33 Years old
99 | Lucky Numbers: 7, 13, 22
100 |
101 | Jane Jackson
102 | 35 Years old
103 | Lucky Numbers: 10, 14, 3
104 |
105 | Jimmy Jackson
106 | 5 Years old
107 | Lucky Numbers: 1
108 | ```
109 |
110 | ## Endpoints
111 |
112 | Esta API debe tener 4 endpoints, todos devuelven JSON:
113 |
114 | ### 1) Obtén todos los miembros de la familia:
115 |
116 | Devuelve todos los miembros de la familia.
117 |
118 | ```md
119 | GET /members
120 |
121 | status_code 200 si se realizó con éxito, 400 si hubo un error por parte del cliente, 500 si el servidor encuentra un error
122 |
123 | RESPONSE BODY (content-type: application/json):
124 |
125 | []
126 | ```
127 |
128 | ### 2) Recupera solo un miembro
129 |
130 | Devuelve el miembro de la familia para el cual `id == member_id`.
131 |
132 | ```md
133 | GET /members/
134 |
135 | RESPONSE (content_type: application/json):
136 |
137 | status_code 200 si se realizó con éxito, 400 si hubo un error por parte del cliente, 500 si el servidor encuentra un error
138 |
139 | body:
140 | {
141 | "id": Int,
142 | "first_name": String,
143 | "age": Int,
144 | "lucky_numbers": List
145 | }
146 | ```
147 |
148 | ### 3) Añadir (POST) un miembro
149 |
150 | Agrega un nuevo miembro a la estructura de datos de la familia.
151 |
152 | ```md
153 | POST /members
154 |
155 | REQUEST BODY (content_type: application/json):
156 | {
157 | id: Int,
158 | first_name: String,
159 | age: Int,
160 | lucky_numbers: []
161 | }
162 |
163 | RESPONSE (content_type: application/json):
164 |
165 | status_code 200 si se realizó con éxito, 400 si hubo un error por parte del cliente, 500 si el servidor encuentra un error
166 | ```
167 |
168 |
169 | ### 4) ELIMINA un miembro
170 |
171 | Elimina el miembro de la familia para el cual `id == member_id`.
172 |
173 | ```md
174 | DELETE /members/
175 |
176 | RESPONSE (content_type: application/json):
177 |
178 | status_code 200 si se realizó con éxito, 400 si hubo un error por parte del cliente, 500 si el servidor encuentra un error
179 |
180 | body: {
181 | done: True
182 | }
183 | ```
184 |
185 | ## Requisitos tecnológicos
186 |
187 | - Todas las solicitudes y respuestas deben estar en content/type: application/json
188 | - Los códigos de respuesta deben ser `200` para solicitudes exitosas, `400` para una solicitud incorrecta o `404` para no encontrados.
189 | - Este ejercicio no incluye una base de datos, todo se debe hacer en durante el tiempo de ejecución del programa (memoria RAM).
190 |
191 | Este y otros proyectos son usados para [aprender a programar](https://4geeksacademy.com/es/aprender-a-programar/aprender-a-programar-desde-cero) por parte de los alumnos de 4Geeks Academy [Coding Bootcamp](https://4geeksacademy.com/us/coding-bootcamp) realizado por [Alejandro Sánchez](https://twitter.com/alesanchezr) y muchos otros contribuyentes. Conoce más sobre nuestros [Cursos de Programación](https://4geeksacademy.com/es/curso-de-programacion-desde-cero?lang=es) para convertirte en [Full Stack Developer](https://4geeksacademy.com/es/coding-bootcamps/desarrollador-full-stack/?lang=es), o nuestro [Data Science Bootcamp](https://4geeksacademy.com/es/coding-bootcamps/curso-datascience-machine-learning).
192 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "e493cea28f791ebdce0ff07e6041756fbcbcd0fe637f1886e99a18343c9dae37"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.13"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "blinker": {
20 | "hashes": [
21 | "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf",
22 | "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"
23 | ],
24 | "markers": "python_version >= '3.9'",
25 | "version": "==1.9.0"
26 | },
27 | "click": {
28 | "hashes": [
29 | "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
30 | "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
31 | ],
32 | "markers": "python_version >= '3.7'",
33 | "version": "==8.1.8"
34 | },
35 | "flask": {
36 | "hashes": [
37 | "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac",
38 | "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"
39 | ],
40 | "index": "pypi",
41 | "markers": "python_version >= '3.9'",
42 | "version": "==3.1.0"
43 | },
44 | "flask-cors": {
45 | "hashes": [
46 | "sha256:6ccb38d16d6b72bbc156c1c3f192bc435bfcc3c2bc864b2df1eb9b2d97b2403c",
47 | "sha256:fa5cb364ead54bbf401a26dbf03030c6b18fb2fcaf70408096a572b409586b0c"
48 | ],
49 | "index": "pypi",
50 | "markers": "python_version >= '3.9' and python_version < '4.0'",
51 | "version": "==5.0.1"
52 | },
53 | "iniconfig": {
54 | "hashes": [
55 | "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
56 | "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
57 | ],
58 | "markers": "python_version >= '3.7'",
59 | "version": "==2.0.0"
60 | },
61 | "itsdangerous": {
62 | "hashes": [
63 | "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef",
64 | "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"
65 | ],
66 | "markers": "python_version >= '3.8'",
67 | "version": "==2.2.0"
68 | },
69 | "jinja2": {
70 | "hashes": [
71 | "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb",
72 | "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"
73 | ],
74 | "markers": "python_version >= '3.7'",
75 | "version": "==3.1.5"
76 | },
77 | "markupsafe": {
78 | "hashes": [
79 | "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4",
80 | "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30",
81 | "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0",
82 | "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9",
83 | "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396",
84 | "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13",
85 | "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028",
86 | "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca",
87 | "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557",
88 | "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832",
89 | "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0",
90 | "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b",
91 | "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579",
92 | "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a",
93 | "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c",
94 | "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff",
95 | "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c",
96 | "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22",
97 | "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094",
98 | "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb",
99 | "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e",
100 | "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5",
101 | "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a",
102 | "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d",
103 | "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a",
104 | "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b",
105 | "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8",
106 | "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225",
107 | "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c",
108 | "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144",
109 | "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f",
110 | "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87",
111 | "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d",
112 | "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93",
113 | "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf",
114 | "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158",
115 | "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84",
116 | "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb",
117 | "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48",
118 | "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171",
119 | "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c",
120 | "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6",
121 | "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd",
122 | "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d",
123 | "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1",
124 | "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d",
125 | "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca",
126 | "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a",
127 | "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29",
128 | "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe",
129 | "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798",
130 | "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c",
131 | "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8",
132 | "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f",
133 | "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f",
134 | "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a",
135 | "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178",
136 | "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0",
137 | "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79",
138 | "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430",
139 | "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"
140 | ],
141 | "markers": "python_version >= '3.9'",
142 | "version": "==3.0.2"
143 | },
144 | "mock": {
145 | "hashes": [
146 | "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744",
147 | "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"
148 | ],
149 | "index": "pypi",
150 | "markers": "python_version >= '3.6'",
151 | "version": "==5.1.0"
152 | },
153 | "packaging": {
154 | "hashes": [
155 | "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
156 | "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
157 | ],
158 | "markers": "python_version >= '3.8'",
159 | "version": "==24.2"
160 | },
161 | "pluggy": {
162 | "hashes": [
163 | "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1",
164 | "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"
165 | ],
166 | "markers": "python_version >= '3.8'",
167 | "version": "==1.5.0"
168 | },
169 | "psycopg2-binary": {
170 | "hashes": [
171 | "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff",
172 | "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5",
173 | "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f",
174 | "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5",
175 | "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0",
176 | "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c",
177 | "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c",
178 | "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341",
179 | "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f",
180 | "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7",
181 | "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d",
182 | "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007",
183 | "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142",
184 | "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92",
185 | "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb",
186 | "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5",
187 | "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5",
188 | "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8",
189 | "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1",
190 | "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68",
191 | "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73",
192 | "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1",
193 | "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53",
194 | "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d",
195 | "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906",
196 | "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0",
197 | "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2",
198 | "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a",
199 | "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b",
200 | "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44",
201 | "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648",
202 | "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7",
203 | "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f",
204 | "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa",
205 | "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697",
206 | "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d",
207 | "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b",
208 | "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526",
209 | "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4",
210 | "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287",
211 | "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e",
212 | "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673",
213 | "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0",
214 | "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30",
215 | "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3",
216 | "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e",
217 | "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92",
218 | "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a",
219 | "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c",
220 | "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8",
221 | "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909",
222 | "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47",
223 | "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864",
224 | "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc",
225 | "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00",
226 | "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb",
227 | "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539",
228 | "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b",
229 | "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481",
230 | "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5",
231 | "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4",
232 | "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64",
233 | "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392",
234 | "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4",
235 | "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1",
236 | "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1",
237 | "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567",
238 | "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"
239 | ],
240 | "index": "pypi",
241 | "markers": "python_version >= '3.8'",
242 | "version": "==2.9.10"
243 | },
244 | "pytest": {
245 | "hashes": [
246 | "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6",
247 | "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"
248 | ],
249 | "index": "pypi",
250 | "markers": "python_version >= '3.8'",
251 | "version": "==8.3.4"
252 | },
253 | "pytest-ordering": {
254 | "hashes": [
255 | "sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2",
256 | "sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6",
257 | "sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6"
258 | ],
259 | "index": "pypi",
260 | "version": "==0.6"
261 | },
262 | "pytest-testdox": {
263 | "hashes": [
264 | "sha256:f3a8f0789d668ccfb60f15aab81fb927b75066cfd19209176166bd7cecae73e6",
265 | "sha256:f48c49c517f0fb926560b383062db4961112078ec6ca555f91692c661bb5c765"
266 | ],
267 | "index": "pypi",
268 | "markers": "python_version >= '3.7'",
269 | "version": "==3.1.0"
270 | },
271 | "werkzeug": {
272 | "hashes": [
273 | "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e",
274 | "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"
275 | ],
276 | "markers": "python_version >= '3.9'",
277 | "version": "==3.1.3"
278 | }
279 | },
280 | "develop": {}
281 | }
282 |
--------------------------------------------------------------------------------