Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
zzstoatzz committed Sep 6, 2024
0 parents commit 4209831
Show file tree
Hide file tree
Showing 13 changed files with 933 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Lint

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Install uv
uses: astral-sh/setup-uv@v2
with:
version: "0.4.6"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"

- name: Install dependencies
run: |
uv pip install -e .[dev]
- name: Run pre-commit
run: pre-commit run --all-files
60 changes: 60 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Python artifacts
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
*.egg
build/
dist/
sdist/

# Test artifacts
.benchmarks/
.coverage
.coverage.*.*
.pytest_cache/

# Type checking artifacts
.mypy_cache/
.dmypy.json
dmypy.json
.pyre/

# IPython
profile_default/
ipython_config.py
*.ipynb_checkpoints/*

# Profiling
/prof

# Environments
.python-version
.env
.venv
env/
venv/

site/
.cache/
**/node_modules

# Databases
*.db

# Editors
.idea/
.vscode/
**/.vscode/

# MacOS
.DS_Store
libcairo.2.dylib

# setuptools-scm generated files
src/**/_version.py
*.log

# specific to this project
.bookkeeping/
python_pack.egg-info/
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.8
hooks:
- id: ruff-format
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
26 changes: 26 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
src = ["src"]

# Use Ruff for sorting imports
lint.extend-select = ["I"]

lint.ignore = [] # add E501 to avoid enforcing line length

[lint.per-file-ignores]
# Do not enforce usage and import order rules in init files
"__init__.py" = ["E402", "F401", "I"]
"main.py" = ["E402", "F401", "I"]

# Do not fix import in compatibility module
"src/python_pack/**/compat.py" = ["F401", "I"]

# Allow wild imports in conftest
"tests/conftest.py" = ["F405", "E402", "F403"]

# Allow fake items in __all__ for runtime
"src/python_pack/utils.py" = ["F822"]

# Do not enforce line length limits in migrations
"src/python_pack/**/database/migrations/**/*" = ["E501"]

[lint.isort]
known-third-party = []
60 changes: 60 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.DEFAULT_GOAL := install
.PHONY: all install clean test dev publish

UV := $(shell which uv)
UV_VERSION := $(shell $(UV) --version | cut -d ' ' -f 2)
VENV_DIR := .venv
REQUIREMENTS := requirements.txt
DEV_REQUIREMENTS := requirements-dev.txt

$(info Using uv @ $(UV))
$(info - version: $(UV_VERSION))

all: install test

.bookkeeping/uv-$(UV_VERSION):
@if command -v uv >/dev/null 2>&1; then \
echo "(uv already installed)"; \
else \
read -p "uv is not installed. Do you want to install it? (y/N) " answer; \
if [ "$$answer" = "y" ] || [ "$$answer" = "Y" ]; then \
mkdir -p .bookkeeping; \
curl -LsSf https://astral.sh/uv/install.sh | sh; \
touch $@; \
else \
echo "Installation cancelled. Please install uv manually to proceed."; \
exit 1; \
fi; \
fi

$(VENV_DIR): .bookkeeping/uv-$(UV_VERSION)
$(UV) venv $(VENV_DIR)

$(REQUIREMENTS): pyproject.toml
$(UV) pip compile pyproject.toml -o $(REQUIREMENTS)

$(DEV_REQUIREMENTS): pyproject.toml
$(UV) pip compile --extra dev pyproject.toml -o $(DEV_REQUIREMENTS)

dev: $(VENV_DIR) $(DEV_REQUIREMENTS)
$(UV) pip sync $(DEV_REQUIREMENTS)
$(UV) pip install -e .[dev]
@echo "ᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖ"
@echo "Virtual environment is ready."
@echo ""
@echo "To activate, run:"
@echo "source $(VENV_DIR)/bin/activate"
@echo "ᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖᨖ"

install: $(VENV_DIR) $(REQUIREMENTS)
$(UV) build
$(UV) pip install dist/*.whl

publish: install
$(UV)x twine upload dist/*

clean:
rm -rf .bookkeeping/ $(VENV_DIR) $(REQUIREMENTS) $(DEV_REQUIREMENTS) dist/

test:
$(VENV_DIR)/bin/python -m pytest
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# want a python environment that just works?
here you go!

```
make clean
make install
make test
```
44 changes: 44 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[project]
name = "python-pack"
dynamic = ["version"]
description = "A modern Python project template"
readme = "README.md"
requires-python = ">=3.12"

dependencies = ["pydantic", "rich", "uv"]

[project.optional-dependencies]

tests = [
"pytest",
"pytest-asyncio",
"pytest-env",
"pytest-rerunfailures",
"pytest-sugar",
"pytest-timeout",
"pytest-xdist",
]

dev = ["ruff", "pre-commit", "python-pack[tests]"]


[tool.setuptools_scm]
write_to = "src/python_pack/_version.py"

# pytest configuration
[tool.pytest.ini_options]
asyncio_mode = 'auto'
asyncio_default_fixture_loop_scope = 'session'
timeout = 20
testpaths = ["tests"]
norecursedirs = [
"*.egg-info",
".git",
".mypy_cache",
".pytest_cache",
".ruff_cache",
".vscode",
"node_modules",
]
env = []
markers = []
78 changes: 78 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --extra dev pyproject.toml -o requirements-dev.txt
annotated-types==0.7.0
# via pydantic
cfgv==3.4.0
# via pre-commit
distlib==0.3.8
# via virtualenv
execnet==2.1.1
# via pytest-xdist
filelock==3.15.4
# via virtualenv
identify==2.6.0
# via pre-commit
iniconfig==2.0.0
# via pytest
markdown-it-py==3.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
nodeenv==1.9.1
# via pre-commit
packaging==24.1
# via
# pytest
# pytest-rerunfailures
# pytest-sugar
platformdirs==4.2.2
# via virtualenv
pluggy==1.5.0
# via pytest
pre-commit==3.8.0
# via python-pack (pyproject.toml)
pydantic==2.9.0
# via python-pack (pyproject.toml)
pydantic-core==2.23.2
# via pydantic
pygments==2.18.0
# via rich
pytest==8.3.2
# via
# python-pack (pyproject.toml)
# pytest-asyncio
# pytest-env
# pytest-rerunfailures
# pytest-sugar
# pytest-timeout
# pytest-xdist
pytest-asyncio==0.24.0
# via python-pack (pyproject.toml)
pytest-env==1.1.3
# via python-pack (pyproject.toml)
pytest-rerunfailures==14.0
# via python-pack (pyproject.toml)
pytest-sugar==1.0.0
# via python-pack (pyproject.toml)
pytest-timeout==2.3.1
# via python-pack (pyproject.toml)
pytest-xdist==3.6.1
# via python-pack (pyproject.toml)
pyyaml==6.0.2
# via pre-commit
rich==13.8.0
# via python-pack (pyproject.toml)
ruff==0.6.4
# via python-pack (pyproject.toml)
termcolor==2.4.0
# via pytest-sugar
typing-extensions==4.12.2
# via
# pydantic
# pydantic-core
tzdata==2024.1
# via pydantic
uv==0.4.6
# via python-pack (pyproject.toml)
virtualenv==20.26.3
# via pre-commit
Empty file added src/python_pack/__init__.py
Empty file.
57 changes: 57 additions & 0 deletions src/python_pack/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from typing import Callable, Literal, get_origin

from pydantic import TypeAdapter


def parse_as[T](
type_: type[T],
data: object,
mode: Literal["python", "json", "strings"] = "python",
) -> T:
"""Parse a given data structure as a Pydantic model via `TypeAdapter`.

Read more about `TypeAdapter` [here](https://docs.pydantic.dev/latest/concepts/type_adapter/).

Args:
type_: The type to parse the data as.
data: The data to be parsed.
mode: The mode to use for parsing, either `python`, `json`, or `strings`.
Defaults to `python`, where `data` should be a Python object (e.g. `dict`).

Returns:
The parsed `data` as the given `type_`.


Example:
```python
from python_pack.utils import parse_as
from pydantic import BaseModel

class ExampleModel(BaseModel):
name: str

# parsing python objects
parsed = parse_as(list[ExampleModel], [{"name": "Marvin"}, {"name": "Arthur"}])
assert isinstance(parsed, list)
assert parsed[0].name == "Marvin"

# parsing json strings
parsed = parse_as(
list[ExampleModel],
'[{"name": "Marvin"}, {"name": "Arthur"}]',
mode="json"
)
assert all(isinstance(item, ExampleModel) for item in parsed)
assert parsed[0].name == "Marvin"
assert parsed[1].name == "Arthur"
```

"""
adapter = TypeAdapter(type_)

if get_origin(type_) is list and isinstance(data, dict):
data = next(iter(data.values()))

parser: Callable[[object], T] = getattr(adapter, f"validate_{mode}")

return parser(data)
Empty file added tests/__init__.py
Empty file.
Loading

0 comments on commit 4209831

Please sign in to comment.