diff --git a/part-01-hello-world/README.md b/part-01-hello-world/README.md index 61e6803..fb8b776 100644 --- a/part-01-hello-world/README.md +++ b/part-01-hello-world/README.md @@ -1,11 +1,248 @@ -## Part 1 Local Setup +# FastAPI -1. `pip install poetry` (or safer, follow the instructions: https://python-poetry.org/docs/#installation) -2. Install dependencies `cd` into the directory where the `pyproject.toml` is located then `poetry install` -3. [UNIX]: Run the FastAPI server via poetry with the bash script: `poetry run ./run.sh` -4. [WINDOWS]: Run the FastAPI server via poetry with the Python command: `poetry run python app/main.py` -5. Open http://localhost:8001/ +## FastAPI course that I deliver to my colleagues. -To stop the server, press CTRL+C +- pip install poetry (or safer, follow the instructions: https://python-poetry.org/docs/#installation) +- pip install requeriments.txt +- Install dependencies cd into the directory where the pyproject.toml is located then poetry install +- [UNIX]: Run the FastAPI server via poetry with the bash script: poetry run ./run.sh +- [WINDOWS]: Run the FastAPI server via poetry with the Python command: poetry run python app/main.py +- Open http://localhost:8001/ +- To stop the server, press CTRL+C -If you get stuck, checkout the [troubleshooting readme](../troubleshooting/README.md) \ No newline at end of file +--- + +## Entornos + +Para crear un entorno virtual en Python en Windows, puedes seguir los pasos a continuación. Usar entornos virtuales es una buena práctica, ya que te permite aislar las dependencias de diferentes proyectos. + +### Pasos para crear un entorno virtual en Windows: + +#### 1. **Instalar Python (si aún no lo tienes)** + +- Primero, asegúrate de tener Python instalado en tu sistema. Puedes verificarlo abriendo el símbolo del sistema (CMD) y ejecutando: + +`python --version` + +Si Python no está instalado, descárgalo de la página oficial: [https://www.python.org/downloads/](https://www.python.org/downloads/) y asegúrate de seleccionar la opción **"Add Python to PATH"** durante la instalación. + +#### 2. **Abrir el Símbolo del Sistema (CMD)** + +- Presiona `Windows + R`, escribe `cmd` y presiona `Enter`. + +#### 3. **Instalar `virtualenv` (opcional)** + +- Python 3.3 y versiones superiores ya incluyen el módulo `venv` para crear entornos virtuales, por lo que no es necesario instalar `virtualenv` a menos que prefieras usarlo. +- Si prefieres usar `virtualenv`, instálalo ejecutando: + + ``` + pip install virtualenv + ``` + +#### 4. **Crear un entorno virtual** + +- Navega a la carpeta de tu proyecto o a la carpeta donde deseas crear el entorno virtual. Usa el comando `cd` para cambiar de directorio, por ejemplo: + + ``` + cd C:\ruta\de\mi\proyecto + ``` + +. Crea el entorno virtual usando el siguiente comando: + +- Crea el entorno virtual usando el siguiente comando: + + - Si usas el módulo `venv` incluido en Python 3: + - ```bash + python -m venv nombre_del_entorno + ``` + + - Si prefieres virtualenv (después de instalarlo): + - ````bash + virtualenv nombre_del_entorno + ``` + ```` + +Esto creará una carpeta llamada nombre_del_entorno en el directorio actual. Dentro de esta carpeta estarán los archivos necesarios para ejecutar el entorno virtual. + +#### 5. **Activar el entorno virtual** + +- Una vez creado el entorno virtual, debes activarlo. Ejecuta el siguiente comando: + + ```bash + nombre_del_entorno\Scripts\activate + ``` + +Si el entorno se activa correctamente, verás el nombre del entorno virtual al principio del prompt del símbolo del sistema. Por ejemplo: + +```bash +(nombre_del_entorno) C:\ruta\de\mi\proyecto> + +``` + +#### 6. **Instalar paquetes en el entorno** + +- Ahora que el entorno está activo, puedes instalar paquetes usando pip y estarán aislados en este entorno. Por ejemplo: + +```bash +pip install nombre_paquete + +``` + +aca hacemos pip install requeriments.txt + +#### 7. **Desactivar el entorno virtual** + +- Cuando termines de trabajar en el entorno virtual, puedes desactivarlo ejecutando: + +```bash +deactivate + +``` + +#### 8. **Ejemplo completo:** + +8.1. Crear el entorno virtual: + +```bash +python -m venv mi_entorno +``` + +8.2. Activar el entorno: + +```bash +mi_entorno\Scripts\activate +``` + +8.3. Instalar paquetes (por ejemplo, FastAPI): + +```bash +pip install fastap +``` + +8.4. Desactivar el entorno: + +```bash +deactivate +``` + +## Introducción a los Tipos de Python + +Estos type hints son una nueva sintaxis, desde Python 3.6+, que permite declarar el tipo de una variable. + +Usando las declaraciones de tipos para tus variables, los editores y otras herramientas pueden proveerte un soporte mejor. + +Este es solo un tutorial corto sobre los Python type hints. Solo cubre lo mínimo necesario para usarlos con FastAPI... realmente es muy poco lo que necesitas. + +Todo FastAPI está basado en estos type hints, lo que le da muchas ventajas y beneficios. + +Pero, así nunca uses FastAPI te beneficiarás de aprender un poco sobre los type hints. + +[Lectura Obligatoria](https://fastapi.tiangolo.com/es/python-types/) + +--- + +## algo mas de Python + +### 1. **Tupla (`tuple`)** + +Una **tupla** es una secuencia ordenada de elementos inmutables. No puedes modificar, añadir o eliminar elementos de una tupla una vez que ha sido creada. + +- **Características** : +- Inmutable: No se puede modificar una vez creada. +- Puede contener elementos duplicados. +- Puede almacenar cualquier tipo de datos. + +```python +#Definición de una tupla +mi_tupla = (1, 2, 3, "a", "b") +print(mi_tupla) # Salida: (1, 2, 3, 'a', 'b') + +#Accediendo a un elemento +print(mi_tupla[0]) # Salida: 1 + +#Intentar modificar una tupla lanzará un error +mi_tupla[0] = 100 # Esto generaría un error +``` + +### 2. **Conjunto (`set`)** + +Un **conjunto** es una colección desordenada de elementos únicos. No permite duplicados, y sus elementos no están indexados ni ordenados. + +- **Características** : +- Desordenado: No tiene un orden específico. +- No se permite duplicados. +- Mutable: Puedes agregar y eliminar elementos. + +```python +# Definición de un set +mi_set = {1, 2, 3, 4, 5, 3, 2} +print(mi_set) # Salida: {1, 2, 3, 4, 5} (Los duplicados se eliminan) + +# Agregar un elemento +mi_set.add(6) +print(mi_set) # Salida: {1, 2, 3, 4, 5, 6} + +# Eliminar un elemento +mi_set.remove(3) +print(mi_set) # Salida: {1, 2, 4, 5, 6} + +``` + +### 3. **Lista (`list`)** + +Una **lista** es una colección ordenada y mutable de elementos. A diferencia de las tuplas, las listas pueden modificarse: puedes agregar, eliminar o cambiar sus elementos. + +- **Características** : +- Ordenada: Mantiene el orden de inserción de los elementos. +- Mutable: Los elementos pueden ser modificados. +- Puede contener elementos duplicados. + +```python +# Definición de una lista +mi_lista = [1, 2, 3, "a", "b", "a"] +print(mi_lista) # Salida: [1, 2, 3, 'a', 'b', 'a'] + +# Agregar un elemento +mi_lista.append(4) +print(mi_lista) # Salida: [1, 2, 3, 'a', 'b', 'a', 4] + +# Eliminar un elemento +mi_lista.remove("a") +print(mi_lista) # Salida: [1, 2, 3, 'b', 'a', 4] + +# Modificar un elemento +mi_lista[0] = 100 +print(mi_lista) # Salida: [100, 2, 3, 'b', 'a', 4] + +``` + +### 4. **Diccionario (`dict`)** + +Un **diccionario** es una colección desordenada de pares clave-valor. Cada clave debe ser única, y se usa para acceder a su valor asociado. + +- **Características** : +- Desordenado (aunque en versiones recientes de Python mantiene el orden de inserción). +- Mutable: Se pueden agregar, eliminar y modificar los pares clave-valor. +- Las claves deben ser únicas, pero los valores pueden repetirse. + +```python +# Definición de un diccionario +mi_dict = {"nombre": "Cristian", "edad": 30, "ciudad": "Buenos Aires"} +print(mi_dict) # Salida: {'nombre': 'Cristian', 'edad': 30, 'ciudad': 'Buenos Aires'} + +# Acceder a un valor por su clave +print(mi_dict["nombre"]) # Salida: Cristian + +# Agregar un nuevo par clave-valor +mi_dict["profesion"] = "Programador" +print(mi_dict) # Salida: {'nombre': 'Cristian', 'edad': 30, 'ciudad': 'Buenos Aires', 'profesion': 'Programador'} + +# Modificar un valor +mi_dict["edad"] = 31 +print(mi_dict) # Salida: {'nombre': 'Cristian', 'edad': 31, 'ciudad': 'Buenos Aires', 'profesion': 'Programador'} + +# Eliminar un par clave-valor +del mi_dict["ciudad"] +print(mi_dict) # Salida: {'nombre': 'Cristian', 'edad': 31, 'profesion': 'Programador'} + +``` diff --git a/part-01-hello-world/app/main.py b/part-01-hello-world/app/main.py index 231e708..dcc624b 100644 --- a/part-01-hello-world/app/main.py +++ b/part-01-hello-world/app/main.py @@ -1,4 +1,7 @@ from fastapi import FastAPI, APIRouter +from datetime import date +from datetime import datetime +import uuid app = FastAPI(title="Recipe API", openapi_url="/openapi.json") @@ -11,7 +14,91 @@ def root() -> dict: """ Root GET """ - return {"msg": "Hello, World!"} + return {"msg": "Hello, Policies"} + + +@api_router.get("/health", status_code=200) +def health() -> dict: + """ + Health Check + """ + return {"status": "ok"} + +@api_router.get("/quiensos", status_code=200) +def health() -> dict: + """ + Quien sos Check + """ + return {"mensaje": "I'm a API an this is one endpoint"} + +@api_router.get("/quiensos/{name}", status_code=200) +async def health(name: str) -> dict: + """ + interactuando con un parametro Check + """ + return {"mensaje": "I'm a FastAPI y vos " + name + " esta enviando un request al que estoy respondiendo"} + + +@api_router.get("/hoy", status_code=200) +async def get_cowsay() -> dict: + """ + Fecha Check + """ + #Día actual + today = date.today() + + #Fecha actual + now = datetime.now() + + return {"status": "ok", "today": today, "now": now} + +@api_router.get("/fibo/{n}", status_code=200) +async def get_fibo(n: int) -> dict: + """ + Devuelve un diccionario con la serie de Fibonacci hasta el n-ésimo término. + + :param n: El número de términos de la serie que se desea obtener. + :return: Diccionario con los términos de la serie Fibonacci. + """ + if n < 1: + return {"error": "El número debe ser mayor o igual a 1"} + + fib_series = {0: 0, 1: 1} + for i in range(2, n): + fib_series[i] = fib_series[i - 1] + fib_series[i - 2] + + return {i: fib_series[i] for i in range(n)} + + +#uuid +def generate_uuid_from_number(number: int) -> str: + """ + Genera un UUID basado en un número dado. + + :param number: Un número entero. + :return: Un UUID generado a partir del número. + """ + # Convertir el número a una cadena + number_str = str(number) + + # Generar un UUID a partir del espacio de nombres y la cadena del número + generated_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, number_str) + + return str(generated_uuid) + + +@app.get("/uuid/{number}") +async def get_uuid_from_number(number: int) -> dict: + """ + Endpoint que recibe un número y devuelve un UUID generado a partir de ese número. + + :param number: Un número entero. + :return: Un diccionario con el UUID generado. + """ + generated_uuid = generate_uuid_from_number(number) + return {"uuid": generated_uuid, "valid_period": "24 hours"} + + app.include_router(api_router) diff --git a/part-02-path-parameters/app/main.py b/part-02-path-parameters/app/main.py index 293a2f8..d6b148b 100644 --- a/part-02-path-parameters/app/main.py +++ b/part-02-path-parameters/app/main.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, APIRouter +from typing import Optional RECIPES = [ { @@ -20,6 +21,25 @@ "source": "Serious Eats", "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", }, + { + "id": 4, + "label": "Masas para sopaipillas", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-masas-para-sopaipillas-77646.html", + }, + { + "id": 5, + "label": "Causa limeña", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-causa-limena-31268.html", + }, + { + "id": 6, + "label": "Torta de manzana sin harina", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-torta-de-manzana-sin-harina-77668.html", + }, + ] @@ -45,8 +65,33 @@ def fetch_recipe(*, recipe_id: int) -> dict: """ result = [recipe for recipe in RECIPES if recipe["id"] == recipe_id] - if result: + # if result: + # return result[0] + try: return result[0] + except IndexError: + return {"status": 404, "error": "Recipe not found"} + + + +# New addition, query parameter +# https://fastapi.tiangolo.com/tutorial/query-params/ +@api_router.get("/search/", status_code=200) +def search_recipes( + keyword: Optional[str] = None, max_results: Optional[int] = 10 +) -> dict: + """ + Search for recipes based on label keyword + """ + if not keyword: + # we use Python list slicing to limit results + # based on the max_results query parameter + return {"results": RECIPES[:max_results]} + + results = filter(lambda recipe: keyword.lower() in recipe["label"].lower(), RECIPES) + return {"results": list(results)[:max_results]} + + app.include_router(api_router) diff --git a/part-04-pydantic-schemas/app/main.py b/part-04-pydantic-schemas/app/main.py index a76a0ff..bf17c83 100644 --- a/part-04-pydantic-schemas/app/main.py +++ b/part-04-pydantic-schemas/app/main.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, APIRouter, Query +from fastapi import FastAPI, APIRouter, Query, HTTPException from typing import Optional @@ -28,8 +28,12 @@ def fetch_recipe(*, recipe_id: int) -> dict: """ result = [recipe for recipe in RECIPES if recipe["id"] == recipe_id] - if result: - return result[0] + # Si no se encuentra, lanzamos una excepción con código 404 + # if not result: + # raise HTTPException(status_code=404, detail=f"Recipe with ID {recipe_id} not found") + + # Si se encuentra, devolvemos la receta (el primer elemento de la lista) + return result[0] # Updated using the FastAPI parameter validation `Query` class @@ -59,7 +63,13 @@ def search_recipes( results = filter(lambda recipe: keyword.lower() in recipe["label"].lower(), RECIPES) return {"results": list(results)[:max_results]} + # results = list(filter(lambda recipe: keyword.lower() in recipe["label"].lower(), RECIPES)) + # if not results: + # raise HTTPException(status_code=404, detail=f"Recipe with LABEL {keyword} not found") + + #return {"results": results[:max_results]} + # New addition, using Pydantic model `RecipeCreate` to define # the POST request body diff --git a/part-04-pydantic-schemas/app/recipe_data.py b/part-04-pydantic-schemas/app/recipe_data.py index d5434fe..7ee499a 100644 --- a/part-04-pydantic-schemas/app/recipe_data.py +++ b/part-04-pydantic-schemas/app/recipe_data.py @@ -17,4 +17,22 @@ "source": "Serious Eats", "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", }, + { + "id": 4, + "label": "Masas para sopaipillas", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-masas-para-sopaipillas-77646.html", + }, + { + "id": 5, + "label": "Causa limeña", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-causa-limena-31268.html", + }, + { + "id": 6, + "label": "Torta de manzana sin harina", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-torta-de-manzana-sin-harina-77668.html", + }, ] diff --git a/part-04-pydantic-schemas/app/schemas.py b/part-04-pydantic-schemas/app/schemas.py index e1bb430..59aba57 100644 --- a/part-04-pydantic-schemas/app/schemas.py +++ b/part-04-pydantic-schemas/app/schemas.py @@ -18,4 +18,4 @@ class RecipeCreate(BaseModel): label: str source: str url: HttpUrl - submitter_id: int + #submitter_id: int diff --git a/part-05-basic-error-handling/app/templates/index.html b/part-05-basic-error-handling/app/templates/index.html new file mode 100644 index 0000000..d0a195c --- /dev/null +++ b/part-05-basic-error-handling/app/templates/index.html @@ -0,0 +1,50 @@ + + + + + + +
+
+
+

+ Recipes - Better than all the REST +

+

+ Latest recipes...

+
+ {% for recipe in recipes %} +
+
+
+

+ {{recipe.label}} +

+

+ + + + + {{recipe.source}} +

+
+
+
+
+

+ View Recipe +

+ +
+
+
+ {% endfor %} +
+
+ + diff --git a/part-06-jinja-templates/app/main.py b/part-06-jinja-templates/app/main.py index c2ae746..892af39 100644 --- a/part-06-jinja-templates/app/main.py +++ b/part-06-jinja-templates/app/main.py @@ -62,8 +62,13 @@ def search_recipes( # based on the max_results query parameter return {"results": RECIPES[:max_results]} - results = filter(lambda recipe: keyword.lower() in recipe["label"].lower(), RECIPES) - return {"results": list(results)[:max_results]} + results = list(filter(lambda recipe: keyword.lower() in recipe["label"].lower(), RECIPES)) + if not results: + raise HTTPException(status_code=404, detail=f"Recipe with LABEL {keyword} not found") + + return {"results": results[:max_results]} + + @api_router.post("/recipe/", status_code=201, response_model=Recipe) diff --git a/part-06-jinja-templates/app/recipe_data.py b/part-06-jinja-templates/app/recipe_data.py index d5434fe..7ee499a 100644 --- a/part-06-jinja-templates/app/recipe_data.py +++ b/part-06-jinja-templates/app/recipe_data.py @@ -17,4 +17,22 @@ "source": "Serious Eats", "url": "http://www.seriouseats.com/recipes/2011/02/cauliflower-and-tofu-curry-recipe.html", }, + { + "id": 4, + "label": "Masas para sopaipillas", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-masas-para-sopaipillas-77646.html", + }, + { + "id": 5, + "label": "Causa limeña", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-causa-limena-31268.html", + }, + { + "id": 6, + "label": "Torta de manzana sin harina", + "source": "Recetas Gratis", + "url": "https://www.recetasgratis.net/receta-de-torta-de-manzana-sin-harina-77668.html", + }, ] diff --git a/part-07-database.zip b/part-07-database.zip new file mode 100644 index 0000000..abdc8ce Binary files /dev/null and b/part-07-database.zip differ diff --git a/part-07-database/alembic.ini b/part-07-database/alembic.ini old mode 100755 new mode 100644 index 921aaf1..2eb3ec3 --- a/part-07-database/alembic.ini +++ b/part-07-database/alembic.ini @@ -68,4 +68,4 @@ formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S +datefmt = %H:%M:%S \ No newline at end of file diff --git a/part-07-database/app/backend_pre_start.py b/part-07-database/app/backend_pre_start.py index 3363a41..ce14228 100644 --- a/part-07-database/app/backend_pre_start.py +++ b/part-07-database/app/backend_pre_start.py @@ -1,8 +1,13 @@ import logging +import sys +import os -from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed # type: ignore + +from db.session import SessionLocal -from app.db.session import SessionLocal logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/part-07-database/app/db/init_db.py b/part-07-database/app/db/init_db.py index 4a15058..87e32f7 100644 --- a/part-07-database/app/db/init_db.py +++ b/part-07-database/app/db/init_db.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -FIRST_SUPERUSER = "admin@recipeapi.com" +FIRST_SUPERUSER = "admin@remanofe.com" # make sure all SQL Alchemy models are imported (app.db.base) before initializing DB # otherwise, SQL Alchemy might fail to initialize relationships properly @@ -23,9 +23,10 @@ def init_db(db: Session) -> None: user = crud.user.get_by_email(db, email=FIRST_SUPERUSER) if not user: user_in = schemas.UserCreate( - full_name="Initial Super User", - email=FIRST_SUPERUSER, - is_superuser=True, + first_name="Initial", + surname="Superuser", + email=FIRST_SUPERUSER, + is_superuser=True, ) user = crud.user.create(db, obj_in=user_in) # noqa: F841 else: diff --git a/part-07-database/app/db/session.py b/part-07-database/app/db/session.py index d04453e..0fb0b3f 100644 --- a/part-07-database/app/db/session.py +++ b/part-07-database/app/db/session.py @@ -1,12 +1,26 @@ -from sqlalchemy import create_engine +from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import OperationalError -SQLALCHEMY_DATABASE_URI = "sqlite:///example.db" +# Cambiamos la URI de conexión para PostgreSQL +SQLALCHEMY_DATABASE_URI = "postgresql://cchiera:Chiera+3@10.100.1.80:5432/remanofe" # 1 - -engine = create_engine( - SQLALCHEMY_DATABASE_URI, - # required for sqlite - connect_args={"check_same_thread": False}, +# Creamos el motor de la base de datos +engine = create_engine( # 2 + SQLALCHEMY_DATABASE_URI + # No es necesario 'connect_args' para PostgreSQL ) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Creamos la sesión +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # 4 + +def test_connection(): + try: + # Creamos una sesión para probar la conexión + with SessionLocal() as session: + session.execute(text("SELECT 1")) # Ejecuta una consulta de prueba + print("Conexión exitosa.") + except OperationalError as e: + print("Error de conexión:", e) + +#test_connection() \ No newline at end of file diff --git a/part-07-database/app/initial_data.py b/part-07-database/app/initial_data.py index 9c9153e..8662937 100644 --- a/part-07-database/app/initial_data.py +++ b/part-07-database/app/initial_data.py @@ -1,9 +1,13 @@ import logging + from app.db.base import Base from app.db.init_db import init_db from app.db.session import SessionLocal + + + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/part-07-database/prestart.sh b/part-07-database/prestart.sh index 006770a..60eb1b5 100755 --- a/part-07-database/prestart.sh +++ b/part-07-database/prestart.sh @@ -6,5 +6,6 @@ python ./app/backend_pre_start.py # Run migrations alembic upgrade head + # Create initial data in DB python ./app/initial_data.py \ No newline at end of file