mirror of
https://github.com/grillazz/fastapi-sqlalchemy-asyncpg.git
synced 2025-08-26 16:40:40 +03:00
Compare commits
14 Commits
dd619e70ce
...
abb6eae23a
Author | SHA1 | Date | |
---|---|---|---|
|
abb6eae23a | ||
|
6ba6d23047 | ||
|
189158d31f | ||
|
4a0944fec6 | ||
|
b26ce25719 | ||
|
9d08ae91ee | ||
|
3143b45a0f | ||
|
ead1b57fbd | ||
|
d798635e3f | ||
|
678bf14786 | ||
|
9a7dff4b62 | ||
|
73b433642f | ||
|
6c1825fd1e | ||
|
2fdc1e9723 |
4
.github/workflows/build-and-test.yml
vendored
4
.github/workflows/build-and-test.yml
vendored
@ -12,8 +12,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [ "3.12" ]
|
python-version: [ "3.13" ]
|
||||||
poetry-version: [ "1.8.3" ]
|
poetry-version: [ "1.8.5" ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PYTHONDONTWRITEBYTECODE: 1
|
PYTHONDONTWRITEBYTECODE: 1
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12-slim-bookworm AS base
|
FROM python:3.13-slim-bookworm AS base
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get upgrade -y \
|
&& apt-get upgrade -y \
|
||||||
&& apt-get install -y --no-install-recommends curl git build-essential \
|
&& apt-get install -y --no-install-recommends curl git build-essential \
|
||||||
@ -10,7 +10,7 @@ FROM base AS install
|
|||||||
WORKDIR /home/code
|
WORKDIR /home/code
|
||||||
|
|
||||||
# allow controlling the poetry installation of dependencies via external args
|
# allow controlling the poetry installation of dependencies via external args
|
||||||
ARG INSTALL_ARGS="--no-root"
|
ARG INSTALL_ARGS="--no-root --no-interaction --no-ansi"
|
||||||
ENV POETRY_HOME="/opt/poetry"
|
ENV POETRY_HOME="/opt/poetry"
|
||||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||||
COPY pyproject.toml poetry.lock ./
|
COPY pyproject.toml poetry.lock ./
|
||||||
|
19
README.md
19
README.md
@ -128,7 +128,7 @@ Setup user authentication with JWT and Redis as token storage.
|
|||||||
### Local development with poetry
|
### Local development with poetry
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pyenv install 3.12 && pyenv local 3.12
|
pyenv install 3.13 && pyenv local 3.13
|
||||||
```
|
```
|
||||||
```shell
|
```shell
|
||||||
poetry install --with dev
|
poetry install --with dev
|
||||||
@ -189,6 +189,7 @@ I've included a few of my favorites to kick things off!
|
|||||||
- **[JUN 8 2024]** implement asyncpg connection pool :fast_forward:
|
- **[JUN 8 2024]** implement asyncpg connection pool :fast_forward:
|
||||||
- **[AUG 17 2024]** granian use case implemented with docker compose and rich logger :fast_forward:
|
- **[AUG 17 2024]** granian use case implemented with docker compose and rich logger :fast_forward:
|
||||||
- **[OCT 16 2024]** apscheduler added to project :fast_forward:
|
- **[OCT 16 2024]** apscheduler added to project :fast_forward:
|
||||||
|
- **[DEC 16 2024]** bump project to Python 3.13 :fast_forward:
|
||||||
|
|
||||||
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
||||||
|
|
||||||
@ -208,21 +209,21 @@ I've included a few of my favorites to kick things off!
|
|||||||
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
|
||||||
[linkedin-url]: https://www.linkedin.com/in/python-has-powers/
|
[linkedin-url]: https://www.linkedin.com/in/python-has-powers/
|
||||||
|
|
||||||
[fastapi.tiangolo.com]: https://img.shields.io/badge/FastAPI-0.115.2-009485?style=for-the-badge&logo=fastapi&logoColor=white
|
[fastapi.tiangolo.com]: https://img.shields.io/badge/FastAPI-0.115.6-009485?style=for-the-badge&logo=fastapi&logoColor=white
|
||||||
[fastapi-url]: https://fastapi.tiangolo.com/
|
[fastapi-url]: https://fastapi.tiangolo.com/
|
||||||
[pydantic.com]: https://img.shields.io/badge/Pydantic-2.9.2-e92063?style=for-the-badge&logo=pydantic&logoColor=white
|
[pydantic.com]: https://img.shields.io/badge/Pydantic-2.10.3-e92063?style=for-the-badge&logo=pydantic&logoColor=white
|
||||||
[pydantic-url]: https://docs.pydantic.dev/latest/
|
[pydantic-url]: https://docs.pydantic.dev/latest/
|
||||||
[sqlalchemy.org]: https://img.shields.io/badge/SQLAlchemy-2.0.36-bb0000?color=bb0000&style=for-the-badge
|
[sqlalchemy.org]: https://img.shields.io/badge/SQLAlchemy-2.0.36-bb0000?color=bb0000&style=for-the-badge
|
||||||
[sqlalchemy-url]: https://docs.sqlalchemy.org/en/20/
|
[sqlalchemy-url]: https://docs.sqlalchemy.org/en/20/
|
||||||
[uvicorn.org]: https://img.shields.io/badge/Uvicorn-0.32.0-2094f3?style=for-the-badge&logo=uvicorn&logoColor=white
|
[uvicorn.org]: https://img.shields.io/badge/Uvicorn-0.34.0-2094f3?style=for-the-badge&logo=uvicorn&logoColor=white
|
||||||
[uvicorn-url]: https://www.uvicorn.org/
|
[uvicorn-url]: https://www.uvicorn.org/
|
||||||
[asyncpg.github.io]: https://img.shields.io/badge/asyncpg-0.29.0-2e6fce?style=for-the-badge&logo=postgresql&logoColor=white
|
[asyncpg.github.io]: https://img.shields.io/badge/asyncpg-0.30.0-2e6fce?style=for-the-badge&logo=postgresql&logoColor=white
|
||||||
[asyncpg-url]: https://magicstack.github.io/asyncpg/current/
|
[asyncpg-url]: https://magicstack.github.io/asyncpg/current/
|
||||||
[pytest.org]: https://img.shields.io/badge/pytest-8.3.3-fff?style=for-the-badge&logo=pytest&logoColor=white
|
[pytest.org]: https://img.shields.io/badge/pytest-8.3.4-fff?style=for-the-badge&logo=pytest&logoColor=white
|
||||||
[pytest-url]: https://docs.pytest.org/en/6.2.x/
|
[pytest-url]: https://docs.pytest.org/en/6.2.x/
|
||||||
[alembic.sqlalchemy.org]: https://img.shields.io/badge/alembic-1.13.3-6BA81E?style=for-the-badge&logo=alembic&logoColor=white
|
[alembic.sqlalchemy.org]: https://img.shields.io/badge/alembic-1.14.0-6BA81E?style=for-the-badge&logo=alembic&logoColor=white
|
||||||
[alembic-url]: https://alembic.sqlalchemy.org/en/latest/
|
[alembic-url]: https://alembic.sqlalchemy.org/en/latest/
|
||||||
[rich.readthedocs.io]: https://img.shields.io/badge/rich-13.9.2-009485?style=for-the-badge&logo=rich&logoColor=white
|
[rich.readthedocs.io]: https://img.shields.io/badge/rich-13.9.4-009485?style=for-the-badge&logo=rich&logoColor=white
|
||||||
[rich-url]: https://rich.readthedocs.io/en/latest/
|
[rich-url]: https://rich.readthedocs.io/en/latest/
|
||||||
[redis.io]: https://img.shields.io/badge/redis-5.1.1-dc382d?style=for-the-badge&logo=redis&logoColor=white
|
[redis.io]: https://img.shields.io/badge/redis-5.2.1-dc382d?style=for-the-badge&logo=redis&logoColor=white
|
||||||
[redis-url]: https://redis.io/
|
[redis-url]: https://redis.io/
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from pydantic import PostgresDsn, RedisDsn, computed_field
|
from pydantic import PostgresDsn, RedisDsn, computed_field, BaseModel
|
||||||
from pydantic_core import MultiHostUrl
|
from pydantic_core import MultiHostUrl
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class SMTPConfig(BaseModel):
|
||||||
|
server: str = os.getenv("EMAIL_HOST", "smtp_server")
|
||||||
|
port: int = os.getenv("EMAIL_PORT", 587)
|
||||||
|
username: str = os.getenv("EMAIL_HOST_USER", "smtp_user")
|
||||||
|
password: str = os.getenv("EMAIL_HOST_PASSWORD", "smtp_password")
|
||||||
|
template_path: str = os.getenv("EMAIL_TEMPLATE_PATH", "templates")
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=".env", env_ignore_empty=True, extra="ignore"
|
env_file=".env", env_ignore_empty=True, extra="ignore"
|
||||||
@ -12,6 +20,8 @@ class Settings(BaseSettings):
|
|||||||
jwt_algorithm: str = os.getenv("JWT_ALGORITHM")
|
jwt_algorithm: str = os.getenv("JWT_ALGORITHM")
|
||||||
jwt_expire: int = os.getenv("JWT_EXPIRE")
|
jwt_expire: int = os.getenv("JWT_EXPIRE")
|
||||||
|
|
||||||
|
smtp: SMTPConfig = SMTPConfig()
|
||||||
|
|
||||||
REDIS_HOST: str
|
REDIS_HOST: str
|
||||||
REDIS_PORT: int
|
REDIS_PORT: int
|
||||||
REDIS_DB: str
|
REDIS_DB: str
|
||||||
|
@ -35,7 +35,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
# Initialize the cache with the redis connection
|
# Initialize the cache with the redis connection
|
||||||
redis_cache = await get_cache()
|
redis_cache = await get_cache()
|
||||||
FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache")
|
FastAPICache.init(RedisBackend(redis_cache), prefix="fastapi-cache")
|
||||||
logger.info(FastAPICache.get_cache_status_header())
|
# logger.info(FastAPICache.get_cache_status_header())
|
||||||
# Initialize the postgres connection pool
|
# Initialize the postgres connection pool
|
||||||
_app.postgres_pool = await asyncpg.create_pool(
|
_app.postgres_pool = await asyncpg.create_pool(
|
||||||
dsn=_postgres_dsn,
|
dsn=_postgres_dsn,
|
||||||
@ -51,7 +51,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
await _app.postgres_pool.close()
|
await _app.postgres_pool.close()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="Stuff And Nonsense API", version="0.16", lifespan=lifespan)
|
app = FastAPI(title="Stuff And Nonsense API", version="0.17", lifespan=lifespan)
|
||||||
|
|
||||||
app.include_router(stuff_router)
|
app.include_router(stuff_router)
|
||||||
app.include_router(nonsense_router)
|
app.include_router(nonsense_router)
|
||||||
|
@ -45,4 +45,3 @@ class SchedulerMiddleware:
|
|||||||
await self.app(scope, receive, send)
|
await self.app(scope, receive, send)
|
||||||
else:
|
else:
|
||||||
await self.app(scope, receive, send)
|
await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
159
app/services/smtp.py
Normal file
159
app/services/smtp.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
from attrs import define, field
|
||||||
|
import smtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
from app.config import settings as global_settings
|
||||||
|
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from pydantic import EmailStr
|
||||||
|
|
||||||
|
from app.utils.logging import AppLogger
|
||||||
|
from app.utils.singleton import SingletonMetaNoArgs
|
||||||
|
|
||||||
|
|
||||||
|
logger = AppLogger().get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class SMTPEmailService(metaclass=SingletonMetaNoArgs):
|
||||||
|
"""
|
||||||
|
SMTPEmailService provides a reusable interface to send emails via an SMTP server.
|
||||||
|
|
||||||
|
This service supports plaintext and HTML emails, and also allows
|
||||||
|
sending template-based emails using the Jinja2 template engine.
|
||||||
|
|
||||||
|
It is implemented as a singleton to ensure that only one SMTP connection is maintained
|
||||||
|
throughout the application lifecycle, optimizing resource usage.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
server_host (str): SMTP server hostname or IP address.
|
||||||
|
server_port (int): Port number for the SMTP connection.
|
||||||
|
username (str): SMTP username for authentication.
|
||||||
|
password (str): SMTP password for authentication.
|
||||||
|
templates (Jinja2Templates): Jinja2Templates instance for loading and rendering email templates.
|
||||||
|
server (smtplib.SMTP): An SMTP object for sending emails, initialized after object creation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# SMTP configuration
|
||||||
|
server_host: str = field(default=global_settings.smtp.server)
|
||||||
|
server_port: int = field(default=global_settings.smtp.port)
|
||||||
|
username: str = field(default=global_settings.smtp.username)
|
||||||
|
password: str = field(default=global_settings.smtp.password)
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
templates: Jinja2Templates = field(
|
||||||
|
factory=lambda: Jinja2Templates(global_settings.smtp.template_path)
|
||||||
|
)
|
||||||
|
server: smtplib.SMTP = field(init=False) # Deferred initialization in post-init
|
||||||
|
|
||||||
|
def __attrs_post_init__(self):
|
||||||
|
"""
|
||||||
|
Initializes the SMTP server connection after the object is created.
|
||||||
|
|
||||||
|
This method sets up a secure connection to the SMTP server, including STARTTLS encryption
|
||||||
|
and logs in using the provided credentials.
|
||||||
|
"""
|
||||||
|
self.server = smtplib.SMTP(self.server_host, self.server_port)
|
||||||
|
self.server.starttls() # Upgrade the connection to secure TLS
|
||||||
|
self.server.login(self.username, self.password)
|
||||||
|
logger.info("SMTPEmailService initialized successfully and connected to SMTP server.")
|
||||||
|
|
||||||
|
def _prepare_email(
|
||||||
|
self,
|
||||||
|
sender: EmailStr,
|
||||||
|
recipients: list[EmailStr],
|
||||||
|
subject: str,
|
||||||
|
body_text: str,
|
||||||
|
body_html: str,
|
||||||
|
) -> MIMEMultipart:
|
||||||
|
"""
|
||||||
|
Prepares a MIME email message with the given plaintext and HTML content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sender (EmailStr): The email address of the sender.
|
||||||
|
recipients (list[EmailStr]): A list of recipient email addresses.
|
||||||
|
subject (str): The subject line of the email.
|
||||||
|
body_text (str): The plaintext content of the email.
|
||||||
|
body_html (str): The HTML content of the email (optional).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MIMEMultipart: A MIME email object ready to be sent.
|
||||||
|
"""
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg["From"] = sender
|
||||||
|
msg["To"] = ",".join(recipients)
|
||||||
|
msg["Subject"] = subject
|
||||||
|
# Add plain text and HTML content (if provided)
|
||||||
|
msg.attach(MIMEText(body_text, "plain"))
|
||||||
|
if body_html:
|
||||||
|
msg.attach(MIMEText(body_html, "html"))
|
||||||
|
logger.debug(f"Prepared email from {sender} to {recipients}.")
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def send_email(
|
||||||
|
self,
|
||||||
|
sender: EmailStr,
|
||||||
|
recipients: list[EmailStr],
|
||||||
|
subject: str,
|
||||||
|
body_text: str = "",
|
||||||
|
body_html: str = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Sends an email to the specified recipients.
|
||||||
|
|
||||||
|
Supports plaintext and HTML email content. This method constructs
|
||||||
|
the email message using `_prepare_email` and sends it using the SMTP server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sender (EmailStr): The email address of the sender.
|
||||||
|
recipients (list[EmailStr]): A list of recipient email addresses.
|
||||||
|
subject (str): The subject line of the email.
|
||||||
|
body_text (str): The plaintext content of the email.
|
||||||
|
body_html (str): The HTML content of the email (optional).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
smtplib.SMTPException: If the email cannot be sent.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
msg = self._prepare_email(sender, recipients, subject, body_text, body_html)
|
||||||
|
self.server.sendmail(sender, recipients, msg.as_string())
|
||||||
|
logger.info(f"Email sent successfully to {recipients} from {sender}.")
|
||||||
|
except smtplib.SMTPException as e:
|
||||||
|
logger.error("Failed to send email", exc_info=e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def send_template_email(
|
||||||
|
self,
|
||||||
|
recipients: list[EmailStr],
|
||||||
|
subject: str,
|
||||||
|
template: str,
|
||||||
|
context: dict,
|
||||||
|
sender: EmailStr,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Sends an email using a Jinja2 template.
|
||||||
|
|
||||||
|
This method renders the template with the provided context and sends it
|
||||||
|
to the specified recipients.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recipients (list[EmailStr]): A list of recipient email addresses.
|
||||||
|
subject (str): The subject line of the email.
|
||||||
|
template (str): The name of the template file in the templates directory.
|
||||||
|
context (dict): A dictionary of values to render the template with.
|
||||||
|
sender (EmailStr): The email address of the sender.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
jinja2.TemplateNotFound: If the specified template is not found.
|
||||||
|
smtplib.SMTPException: If the email cannot be sent.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
template_str = self.templates.get_template(template)
|
||||||
|
body_html = template_str.render(context) # Render the HTML using context variables
|
||||||
|
self.send_email(sender, recipients, subject, body_html=body_html)
|
||||||
|
logger.info(f"Template email sent successfully to {recipients} using template {template}.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to send template email", exc_info=e)
|
||||||
|
raise
|
@ -16,3 +16,21 @@ class SingletonMeta(type):
|
|||||||
instance = super().__call__(*args, **kwargs)
|
instance = super().__call__(*args, **kwargs)
|
||||||
cls._instances[cls] = instance
|
cls._instances[cls] = instance
|
||||||
return cls._instances[cls]
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
|
class SingletonMetaNoArgs(type):
|
||||||
|
"""
|
||||||
|
Singleton metaclass for classes without parameters on constructor,
|
||||||
|
for compatibility with FastApi Depends() function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instances = {}
|
||||||
|
|
||||||
|
_lock: Lock = Lock()
|
||||||
|
|
||||||
|
def __call__(cls):
|
||||||
|
with cls._lock:
|
||||||
|
if cls not in cls._instances:
|
||||||
|
instance = super().__call__()
|
||||||
|
cls._instances[cls] = instance
|
||||||
|
return cls._instances[cls]
|
||||||
|
3376
poetry.lock
generated
3376
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,39 +1,40 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "fastapi-sqlalchemy-asyncpg"
|
name = "fastapi-sqlalchemy-asyncpg"
|
||||||
version = "0.0.16"
|
version = "0.0.17"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Jakub Miazek <the@grillazz.com>"]
|
authors = ["Jakub Miazek <the@grillazz.com>"]
|
||||||
packages = []
|
packages = []
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.13"
|
||||||
fastapi = {version = "^0.115.2", extras = ["all"]}
|
fastapi = {version = "^0.115.6", extras = ["all"]}
|
||||||
pydantic = {version = "^2.9.2", extras = ["email"]}
|
pydantic = {version = "^2.10.3", extras = ["email"]}
|
||||||
pydantic-settings = "^2.6.0"
|
pydantic-settings = "^2.7.0"
|
||||||
sqlalchemy = "^2.0.36"
|
sqlalchemy = "^2.0.36"
|
||||||
uvicorn = { version = "^0.32.0", extras = ["standard"]}
|
uvicorn = { version = "^0.34.0", extras = ["standard"]}
|
||||||
asyncpg = "^0.29.0"
|
asyncpg = "^0.30.0"
|
||||||
alembic = "^1.13.3"
|
alembic = "^1.14.0"
|
||||||
httpx = "^0.27.2"
|
httpx = "^0.28.1"
|
||||||
pytest = "^8.3.3"
|
pytest = "^8.3.4"
|
||||||
pytest-cov = "^5.0.0"
|
pytest-cov = "^6.0.0"
|
||||||
uvloop = "^0.21.0"
|
uvloop = "^0.21.0"
|
||||||
httptools = "^0.6.4"
|
httptools = "^0.6.4"
|
||||||
rich = "^13.9.2"
|
rich = "^13.9.4"
|
||||||
pyjwt = {version = "^2.9.0", extras = ["cryptography"]}
|
pyjwt = {version = "^2.10.1", extras = ["cryptography"]}
|
||||||
redis = "^5.1.1"
|
redis = "^5.2.1"
|
||||||
bcrypt = "^4.2.0"
|
bcrypt = "^4.2.1"
|
||||||
polars = "^1.9.0"
|
polars = "^1.17.1"
|
||||||
python-multipart = "^0.0.12"
|
python-multipart = "^0.0.20"
|
||||||
fastexcel = "^0.12.0"
|
fastexcel = "^0.12.0"
|
||||||
fastapi-cache2 = {git = "https://github.com/yoco-tech/fastapi-cache.git", rev = "main"}
|
fastapi-cache2 = "^0.2.1"
|
||||||
inline-snapshot = "^0.13.3"
|
inline-snapshot = "^0.17.0"
|
||||||
dirty-equals = "^0.8.0"
|
dirty-equals = "^0.8.0"
|
||||||
polyfactory = "^2.17.0"
|
polyfactory = "^2.18.1"
|
||||||
granian = "^1.6.1"
|
granian = "^1.7.0"
|
||||||
transformers = "^4.45.2"
|
|
||||||
apscheduler = {version = "^4.0.0a5", extras = ["redis,sqlalchemy"]}
|
apscheduler = {version = "^4.0.0a5", extras = ["redis,sqlalchemy"]}
|
||||||
|
pendulum = {git = "https://github.com/sdispater/pendulum.git", rev="develop"}
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
devtools = { extras = ["pygments"], version = "^0.12.2" }
|
devtools = { extras = ["pygments"], version = "^0.12.2" }
|
||||||
@ -58,8 +59,8 @@ lint.ignore = ["E501"]
|
|||||||
|
|
||||||
# Exclude a variety of commonly ignored directories.
|
# Exclude a variety of commonly ignored directories.
|
||||||
exclude = ["alembic",]
|
exclude = ["alembic",]
|
||||||
# Assume Python 3.12
|
# Assume Python 3.13
|
||||||
target-version = "py312"
|
target-version = "py313"
|
||||||
|
|
||||||
[tool.ruff.lint.flake8-quotes]
|
[tool.ruff.lint.flake8-quotes]
|
||||||
docstring-quotes = "double"
|
docstring-quotes = "double"
|
||||||
|
@ -32,7 +32,7 @@ async def test_add_user(client: AsyncClient):
|
|||||||
response.json()["access_token"], options={"verify_signature": False}
|
response.json()["access_token"], options={"verify_signature": False}
|
||||||
)
|
)
|
||||||
assert claimset["expiry"] == IsPositiveFloat()
|
assert claimset["expiry"] == IsPositiveFloat()
|
||||||
assert claimset["platform"] == "python-httpx/0.27.2"
|
assert claimset["platform"] == "python-httpx/0.28.1"
|
||||||
|
|
||||||
|
|
||||||
# TODO: parametrize test with diff urls including 404 and 401
|
# TODO: parametrize test with diff urls including 404 and 401
|
||||||
@ -49,7 +49,7 @@ async def test_get_token(client: AsyncClient):
|
|||||||
)
|
)
|
||||||
assert claimset["email"] == payload["email"]
|
assert claimset["email"] == payload["email"]
|
||||||
assert claimset["expiry"] == IsPositiveFloat()
|
assert claimset["expiry"] == IsPositiveFloat()
|
||||||
assert claimset["platform"] == "python-httpx/0.27.2"
|
assert claimset["platform"] == "python-httpx/0.28.1"
|
||||||
|
|
||||||
|
|
||||||
# TODO: baerer token test
|
# TODO: baerer token test
|
||||||
|
Loading…
x
Reference in New Issue
Block a user