Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Template azure function #3

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .funcignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.git*
.vscode
local.settings.json
test
.venv
24 changes: 15 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ htmlcov/
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

Expand All @@ -59,7 +58,6 @@ coverage.xml
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
Expand Down Expand Up @@ -87,16 +85,12 @@ ipython_config.py
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# having no cross-platform support, pipenv may install dependencies that dont work, or not
# install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
# celery beat schedule file
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py
Expand Down Expand Up @@ -128,8 +122,20 @@ dmypy.json
# Pyre type checker
.pyre/

# Azure Functions artifacts
bin
obj
appsettings.json
local.settings.json

# Azurite artifacts
__blobstorage__
__queuestorage__
__azurite_db*__.json
.python_packages

# Log files
*.log

# DB Files
*.db
*.db
6 changes: 6 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
"ms-azuretools.vscode-azurefunctions",
"ms-python.python"
]
}
12 changes: 12 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to Python Functions",
"type": "python",
"request": "attach",
"port": 9091,
"preLaunchTask": "func: host start"
}
]
}
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"azureFunctions.deploySubpath": ".",
"azureFunctions.scmDoBuildDuringDeployment": true,
"azureFunctions.pythonVenv": ".venv",
"azureFunctions.projectLanguage": "Python",
"azureFunctions.projectRuntime": "~4",
"debug.internalConsoleOptions": "neverOpen"
}
26 changes: 26 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "func",
"command": "host start",
"problemMatcher": "$func-python-watch",
"isBackground": true,
"dependsOn": "pip install (functions)"
},
{
"label": "pip install (functions)",
"type": "shell",
"osx": {
"command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt"
},
"windows": {
"command": "${config:azureFunctions.pythonVenv}\\Scripts\\python -m pip install -r requirements.txt"
},
"linux": {
"command": "${config:azureFunctions.pythonVenv}/bin/python -m pip install -r requirements.txt"
},
"problemMatcher": []
}
]
}
Empty file.
7 changes: 7 additions & 0 deletions app/business/employee_management/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from fastapi import APIRouter

from app.business.employee_management.controllers import employee

# Include all routers here
api_router = APIRouter()
api_router.include_router(employee.router, tags=["users"])
18 changes: 18 additions & 0 deletions app/business/employee_management/controllers/employee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging

from fastapi import APIRouter, Depends

from app.business.employee_management.services.employee import EmployeeService

router = APIRouter(prefix="/users")

logger = logging.getLogger(__name__)


@router.get("/info")
async def info(employee_service: EmployeeService = Depends(EmployeeService)):
logger.info("TEST INFO")
logger.error("TEST ERROR")
logger.debug("TEST DEBUG")
await employee_service.get_user_by_email(email="[email protected]")
return {}
Empty file.
Empty file.
17 changes: 17 additions & 0 deletions app/business/employee_management/services/employee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Optional

from fastapi import Depends

from app.common.exceptions.http import NotFoundException
from app.domain.models import Employee
from app.domain.repositories.employee import get_employee_repository, EmployeeRepository


class EmployeeService:
def __init__(self, repository: EmployeeRepository = Depends(get_employee_repository)):
self.user_repo = repository

async def get_user_by_email(self, email: str) -> Optional[Employee]:
user = await self.user_repo.get_by_email(email=email)
if user is None:
raise NotFoundException(detail="User not found")
5 changes: 5 additions & 0 deletions app/business/todo_management/controllers/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ async def create_todo(create_request: CreateTodoRequest, todo_service=Depends(To
logger.info("Creating a new TODO")
todo = await todo_service.create_todo(create_request)
return todo

@router.get("/test")
async def get_pending_todos():
logger.info("Retrieving all the pending TODOs")
return "Hello"
4 changes: 2 additions & 2 deletions app/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Callable, Any
from typing import Callable, Any, Union
from fastapi_keycloak import OIDCUser
from app.common.infra import idp


# Shortcut for checking current user and roles
def get_user(required_roles: list[str] | None = None) -> Callable[[], OIDCUser]:
def get_user(required_roles: Union[list[str], None] = None) -> Callable[[], OIDCUser]:
"""Returns a function that checks the current user based on an access token in the HTTP-header. Optionally verifies
roles are possessed by the user

Expand Down
2 changes: 1 addition & 1 deletion app/common/base/base_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


class BaseSQLRepository(Generic[ModelType]):

def __init__(self, model: Type[ModelType], session: AsyncSession = Depends(get_async_session)):
"""
Object with default methods to Create, Read, Update and Delete (CRUD).
Expand Down
6 changes: 3 additions & 3 deletions app/common/core/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import os
from functools import lru_cache
from typing import Type, List
from typing import Type, List, Union

import yaml
from pydantic import BaseSettings, AnyHttpUrl, validator
Expand All @@ -14,10 +14,10 @@ class GlobalSettings(BaseSettings):
environment: str = "TEST"
port: int = 80
swagger_path: str = "/docs"
cors: List[str] | List[AnyHttpUrl] = []
cors: Union[List[str], List[AnyHttpUrl]] = []

@validator("cors", pre=True)
def assemble_cors_origins(cls, v: str | List[str]) -> List[str] | str:
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
if v is None:
print("CORS Not Specified")
return []
Expand Down
72 changes: 72 additions & 0 deletions app/common/core/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from fastapi import Depends
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.future import Engine
from sqlalchemy.orm import sessionmaker
from sqlmodel import SQLModel, create_engine, Session

from .configuration import DatabaseSettings, get_db_settings


def get_db_uri(db_settings: DatabaseSettings = Depends(get_db_settings)) -> str:
if db_settings.type == "LOCAL":
uri = "sqlite:///{}.db".format(db_settings.database)
elif db_settings.type == "PG":
uri = "postgresql+psycopg2://{0}:{1}@{2}:{3}/{4}".format(
db_settings.username, db_settings.password, db_settings.host, db_settings.port, db_settings.database)
else:
uri = "sqlite:///database.db"
return uri


def get_async_db_uri(db_settings: DatabaseSettings = Depends(get_db_settings)) -> str:
if db_settings.type == "LOCAL":
uri = "sqlite+aiosqlite:///{}.db".format(db_settings.database)
elif db_settings.type == "PG":
uri = "postgresql+psycopg2://{0}:{1}@{2}:{3}/{4}".format(
db_settings.username, db_settings.password, db_settings.host, db_settings.port, db_settings.database)
else:
uri = "sqlite+aiosqlite:///database.db"
return uri


def get_db_engine(settings: DatabaseSettings = Depends(get_db_settings), db_uri: str = Depends(get_db_uri)) -> Engine:
return create_engine(db_uri, echo=settings.enable_logs, echo_pool=settings.enable_logs, pool_pre_ping=True)


def get_async_db_engine(settings: DatabaseSettings = Depends(get_db_settings), db_uri: str = Depends(get_async_db_uri)):
return create_async_engine(db_uri,
echo=settings.enable_logs, echo_pool=settings.enable_logs,
future=True, pool_pre_ping=True)


def get_session(engine: Engine = Depends(get_db_engine)):
sess = None
try:
sess = Session(engine)
yield sess
finally:
if sess:
sess.close()


def init_db_entities(db: DatabaseSettings):
engine = get_db_engine(db, get_db_uri(db))
SQLModel.metadata.create_all(engine)

#
# def get_db_session_factory(engine: Engine = Depends(get_async_db_engine)):
# """
# Generates a session factory from the configured SQL Engine
# """
# return sessionmaker(autocommit=False, class_=AsyncSession, autoflush=False, bind=engine)

#
# def get_db(session_factory: sessionmaker = Depends(get_session_factory)) -> Generator:
# """
# Generates a database session from the configured factory // TODO: Problem with async close
# """
# try:
# db = session_factory()
# yield db
# finally:
# db.close()
3 changes: 2 additions & 1 deletion app/common/infra/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from enum import Enum
from typing import Union

from fastapi import FastAPI
from fastapi_keycloak import FastAPIKeycloak
Expand All @@ -10,7 +11,7 @@ class IDPType(Enum):
KEYCLOAK = 0


def get_idp(keycloak_settings: KeycloakSettings | None):
def get_idp(keycloak_settings: Union[KeycloakSettings, None]):
# Check if configuration is defined to use Keycloak IDP
if keycloak_settings is None or keycloak_settings.auth_server is None or keycloak_settings.realm is None:
return None, None
Expand Down
4 changes: 4 additions & 0 deletions app/domain/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Import DB Entities for init
# Tables must extend SQLModel
from .todo import Todo
from .employee import Employee
11 changes: 11 additions & 0 deletions app/domain/models/employee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import Optional
from pydantic import EmailStr
from sqlmodel import Field
from app.common.base.base_entity import BaseUUIDModel


# DB ENTITY
class Employee(BaseUUIDModel, table=True):
name: Optional[str] = Field(nullable=True)
surname: Optional[str] = Field(nullable=True)
mail: EmailStr = Field(nullable=False, index=True, sa_column_kwargs={"unique": True})
8 changes: 8 additions & 0 deletions app/domain/models/todo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from sqlmodel import Field
from app.common.base.base_entity import BaseUUIDModel


# DB ENTITY
class Todo(BaseUUIDModel, table=True):
description: str = Field(nullable=False)
done: bool = Field(nullable=False, default=False)
Empty file.
31 changes: 31 additions & 0 deletions app/domain/repositories/employee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Optional

from fastapi import Depends
from sqlalchemy.orm import sessionmaker
from sqlmodel import select, Session

from app.common.base.base_repository import BaseRepository
from app.common.core.database import get_session
from app.common.exceptions.http import NotFoundException
from app.domain.models.employee import Employee


class EmployeeRepository(BaseRepository[Employee]):

def get_by_email(self, *, email: str) -> Optional[Employee]:
employees = self.session.exec(select(Employee).where(Employee.mail == email))
employee = employees.one_or_none()
if not employee:
raise NotFoundException(detail="Employee with email {} not found".format(email))
return employee

def create(self, *, email: str, name: Optional[str] = None, surname: Optional[str]) -> Employee:
new_employee = Employee(email=email, name=name, surname=surname)
self.session.add(new_employee)
self.session.commit()
self.session.refresh(new_employee)
return new_employee


def get_employee_repository(session: Session = Depends(get_session)):
return EmployeeRepository(Employee, session)
Loading