Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
lesnik512 committed Sep 12, 2024
0 parents commit 1355581
Show file tree
Hide file tree
Showing 16 changed files with 445 additions and 0 deletions.
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: main

on:
push:
branches:
- main
pull_request: {}

concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.10"
- uses: extractions/setup-just@v2
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
- run: just install lint-ci

pytest:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- uses: extractions/setup-just@v2
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
- run: just install test . --cov=. --cov-report xml
- name: Upload coverage to Codecov
uses: codecov/[email protected]
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
flags: unittests
name: codecov-${{ matrix.python-version }}
24 changes: 24 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Publish Package

on:
release:
types:
- published

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/cache@v4
with:
path: ~/.cache/uv
key: publish-${{ hashFiles('pyproject.toml') }}
- uses: extractions/setup-just@v2
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
- run: just publish
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generic things
*.pyc
*~
__pycache__/*
*.swp
*.sqlite3
*.map
.vscode
.idea
.DS_Store
.env
.mypy_cache
.pytest_cache
.ruff_cache
.coverage
htmlcov/
coverage.xml
pytest.xml
dist/
.python-version
.venv
uv.lock
24 changes: 24 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
default: install lint test

install:
uv lock --upgrade
uv sync --all-extras --frozen

lint:
uv run ruff format .
uv run ruff check . --fix
uv run mypy .

lint-ci:
uv run ruff format . --check
uv run ruff check . --no-fix
uv run mypy .

test *args:
uv run pytest {{ args }}

publish:
rm -rf dist/*
uv tool run --from build python -m build --installer uv
uv tool run twine check dist/*
uv tool run twine upload dist/* --username __token__ --password $PYPI_TOKEN
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Lite-Bootstrap package
==
[![Test Coverage](https://codecov.io/gh/modern-python/lite-bootstrap/branch/main/graph/badge.svg)](https://codecov.io/gh/modern-python/lite-bootstrap)
[![MyPy Strict](https://img.shields.io/badge/mypy-strict-blue)](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
[![Supported versions](https://img.shields.io/pypi/pyversions/lite-bootstrap.svg)](https://pypi.python.org/pypi/lite-bootstrap)
[![downloads](https://img.shields.io/pypi/dm/lite-bootstrap.svg)](https://pypistats.org/packages/lite-bootstrap)
[![GitHub stars](https://img.shields.io/github/stars/modern-python/lite-bootstrap)](https://github.com/modern-python/lite-bootstrap/stargazers)

This package helps to build new microservices

## Quickstart:
### Installation

```shell
$ pip install lite-bootstrap
```
Empty file added lite_bootstrap/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions lite_bootstrap/fastapi_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import dataclasses

import fastapi
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware

from lite_bootstrap.opentelemetry_bootstrap import OpenTelemetryBootstrap
from lite_bootstrap.sentry_bootstrap import SentryBootstrap


@dataclasses.dataclass(kw_only=True, slots=True)
class FastAPIBootstrap:
app: fastapi.FastAPI
opentelemetry: OpenTelemetryBootstrap
sentry: SentryBootstrap
opentelemetry_excluded_urls: list[str] = dataclasses.field(default_factory=list)

def bootstrap_init(self) -> None:
if self.sentry.sentry_dsn:
self.sentry.start_tracing()
self.app.add_middleware(SentryAsgiMiddleware)

self.opentelemetry.start_tracing()
if self.opentelemetry.endpoint:
FastAPIInstrumentor.instrument_app(
app=self.app,
tracer_provider=self.opentelemetry.tracer_provider,
excluded_urls=",".join(self.opentelemetry_excluded_urls),
)

def teardown(self) -> None:
self.opentelemetry.teardown()
if self.opentelemetry.endpoint:
FastAPIInstrumentor.uninstrument_app(self.app)
46 changes: 46 additions & 0 deletions lite_bootstrap/opentelemetry_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import dataclasses

from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]
from opentelemetry.sdk import resources
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.trace import set_tracer_provider


@dataclasses.dataclass(kw_only=True)
class OpenTelemetryBootstrap:
endpoint: str
service_name: str
instruments: list[BaseInstrumentor] = dataclasses.field(default_factory=list)
tracer_provider: TracerProvider | None = dataclasses.field(init=False)

def start_tracing(self) -> None:
if not self.endpoint:
return

self.tracer_provider: TracerProvider = TracerProvider(
resource=resources.Resource.create({resources.SERVICE_NAME: self.service_name}),
)
self.tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint=self.endpoint,
insecure=True,
),
),
)

for instrument in self.instruments:
instrument.instrument(
tracer_provider=self.tracer_provider,
)

set_tracer_provider(self.tracer_provider)

def teardown(self) -> None:
if not self.endpoint:
return

for instrument in self.instruments:
instrument.uninstrument()
Empty file added lite_bootstrap/py.typed
Empty file.
32 changes: 32 additions & 0 deletions lite_bootstrap/sentry_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import dataclasses
import typing

import sentry_sdk


@dataclasses.dataclass(kw_only=True)
class SentryBootstrap:
sentry_dsn: str
environment: str | None = None
release: str | None = None
max_breadcrumbs: int = 15
attach_stacktrace: bool = True
default_integrations: bool = True
sentry_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
tags: dict[str, str] | None = None

def start_tracing(self) -> None:
if not self.sentry_dsn:
return

sentry_sdk.init(
dsn=self.sentry_dsn,
environment=self.environment,
max_breadcrumbs=self.max_breadcrumbs,
attach_stacktrace=self.attach_stacktrace,
default_integrations=self.default_integrations,
release=self.release,
**self.sentry_params,
)
tags: dict[str, str] = self.tags or {}
sentry_sdk.set_tags(tags)
102 changes: 102 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
[project]
name = "lite-bootstrap"
description = "Lite package for bootstrapping new microservices"
authors = [
{ name = "Artur Shiriev", email = "[email protected]" },
]
readme = "README.md"
requires-python = ">=3.10,<4"
license = "MIT"
keywords = [
"python",
"microservice",
"bootstrap",
"opentelemetry",
"sentry",
"error-tracing",
"fastapi",
]
classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Typing :: Typed",
"Topic :: Software Development :: Libraries",
]
dynamic = ["version"]
packages = [
{ include = "lite_bootstrap" },
]

[project.urls]
repository = "https://github.com/modern-python/lite-bootstrap"

[project.optional-dependencies]
tracing = [
"sentry-sdk",
"opentelemetry-api",
"opentelemetry-sdk",
"opentelemetry-exporter-otlp",
"opentelemetry-instrumentation",
]
fastapi = [
"fastapi",
"sentry-sdk",
"opentelemetry-api",
"opentelemetry-sdk",
"opentelemetry-exporter-otlp",
"opentelemetry-instrumentation",
"opentelemetry-instrumentation-fastapi",
]

[tool.uv]
dev-dependencies = [
"pytest",
"pytest-cov",
"httpx", # for test client
"mypy",
"ruff",
]

[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "vcs"

[tool.mypy]
python_version = "3.10"
strict = true

[tool.ruff]
fix = true
unsafe-fixes = true
line-length = 120
target-version = "py310"

[tool.ruff.format]
docstring-code-format = true

[tool.ruff.lint]
select = ["ALL"]
ignore = [
"D1", # allow missing docstrings
"S101", # allow asserts
"TCH", # ignore flake8-type-checking
"FBT", # allow boolean args
"ANN101", # missing-type-self
"ANN102", # missing-type-cls
"D203", # "one-blank-line-before-class" conflicting with D211
"D213", # "multi-line-summary-second-line" conflicting with D212
"COM812", # flake8-commas "Trailing comma missing"
"ISC001", # flake8-implicit-str-concat
]
isort.lines-after-imports = 2
isort.no-lines-before = ["standard-library", "local-folder"]

[tool.pytest.ini_options]
addopts = "--cov=. --cov-report term-missing"

[tool.coverage.report]
exclude_also = ["if typing.TYPE_CHECKING:"]
Empty file added tests/__init__.py
Empty file.
27 changes: 27 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import typing

import pytest
from fastapi import APIRouter, FastAPI
from fastapi.responses import JSONResponse
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor # type: ignore[attr-defined]


class CustomInstrumentor(BaseInstrumentor): # type: ignore[misc]
def instrumentation_dependencies(self) -> typing.Collection[str]:
return []

def _uninstrument(self, **kwargs: typing.Mapping[str, typing.Any]) -> None:
pass


@pytest.fixture
def fastapi_app() -> FastAPI:
app: typing.Final = FastAPI()
router: typing.Final = APIRouter()

@router.get("/test")
async def for_test_endpoint() -> JSONResponse:
return JSONResponse(content={"key": "value"})

app.include_router(router)
return app
Loading

0 comments on commit 1355581

Please sign in to comment.