├── 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 | 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 | --------------------------------------------------------------------------------