Initial code dump

This is the initial code dump from Janik.
I obtained express permission to license it under MIT, see the next
commit.
This commit is contained in:
Janik Haag 2024-10-20 16:16:34 +02:00 committed by Raito Bezarius
commit 9e0b8e5699
39 changed files with 1342 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

165
.gitignore vendored Normal file
View file

@ -0,0 +1,165 @@
.direnv/
result*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# 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
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm-project.org/#use-with-ide
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View file

@ -0,0 +1,76 @@
From 572164ab51e22f82f4990a69988f09ae23743727 Mon Sep 17 00:00:00 2001
From: "Janik H." <janik@aq0.de>
Date: Tue, 17 Sep 2024 17:58:36 +0200
Subject: [PATCH] django-htmx: init at 1.19.0
Change-Id: I67aed9c0ccfc26e06687e4c5e8c01b65b68a6c80
---
.../python-modules/django-htmx/default.nix | 42 +++++++++++++++++++
pkgs/top-level/python-packages.nix | 2 +
2 files changed, 44 insertions(+)
create mode 100644 pkgs/development/python-modules/django-htmx/default.nix
diff --git a/pkgs/development/python-modules/django-htmx/default.nix b/pkgs/development/python-modules/django-htmx/default.nix
new file mode 100644
index 000000000000..24569bfc60c2
--- /dev/null
+++ b/pkgs/development/python-modules/django-htmx/default.nix
@@ -0,0 +1,42 @@
+{
+ lib,
+ buildPythonPackage,
+ django,
+ fetchFromGitHub,
+ asgiref,
+ setuptools,
+ pythonOlder,
+}:
+
+buildPythonPackage rec {
+ pname = "django-htmx";
+ version = "1.19.0";
+ pyproject = true;
+
+ disabled = pythonOlder "3.8";
+ doCheck = false;
+
+ src = fetchFromGitHub {
+ owner = "adamchainz";
+ repo = "django-htmx";
+ rev = "refs/tags/${version}";
+ hash = "sha256-nSutErUkFafKjBswhC+Lrn39MgCbCrzttAx1a+qt1so=";
+ };
+
+ build-system = [ setuptools ];
+
+ dependencies = [
+ asgiref
+ django
+ ];
+
+ pythonImportsCheck = [ "django_htmx" ];
+
+ meta = with lib; {
+ description = "Some tools to make it easier to use htmx in your Django projects";
+ homepage = "https://django-htmx.readthedocs.io/en/latest";
+ changelog = "https://github.com/adamchainz/django-htmx/blob/${version}/docs/changelog.rst";
+ license = licenses.bsd3;
+ maintainers = with maintainers; [ derdennisop ];
+ };
+}
diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix
index 6e673f7f6a83..b5de543ef02e 100644
--- a/pkgs/top-level/python-packages.nix
+++ b/pkgs/top-level/python-packages.nix
@@ -3371,6 +3371,8 @@ self: super: with self; {
django-hijack = callPackage ../development/python-modules/django-hijack { };
+ django-htmx = callPackage ../development/python-modules/django-htmx { };
+
django-i18nfield = callPackage ../development/python-modules/django-i18nfield { };
django-import-export = callPackage ../development/python-modules/django-import-export { };
--
2.46.0

1
README.md Normal file
View file

@ -0,0 +1 @@
# forkos-issue-tracker

66
default.nix Normal file
View file

@ -0,0 +1,66 @@
let
pins = import ./npins;
in {
pkgs ? let
rawPkgs = import pins.nixpkgs { };
patchedNixpkgs = rawPkgs.applyPatches {
name = "nixpkgs";
src = pins.nixpkgs;
patches = [
(rawPkgs.fetchpatch {
name = "python3Packages.django-simple-history: change to pyproject";
url = "https://cl.forkos.org/changes/nixpkgs~203/revisions/1/patch?download";
hash = "sha256-lR6hW+yJcmCyKafZU9c/XSUE1nwvjdq94I254lAZLTE=";
decode = "base64 -d";
})
./0001-django-htmx-init-at-1.19.0.patch
];
};
in import patchedNixpkgs { }
}: rec {
shell = pkgs.mkShell {
buildInputs = with pkgs; [
npins
gitFull
pdm
sqlite-interactive
forkosIssueTracker
];
};
htmx = pkgs.fetchurl {
url = "https://unpkg.com/browse/htmx.org@1.9.12/dist/htmx.min.js";
hash = "sha256-ruX9vUDsKIabkzupk4YAyEbSEk37QyOHjNneN9eyc4g=";
};
forkosIssueTracker = pkgs.python3.pkgs.buildPythonApplication {
pname = "forkosissuetracker";
version = "0.1.0";
pyproject = true;
src = ./.;
disabled = pkgs.python3.pkgs.pythonOlder "3.12";
nativeBuildInputs = with pkgs.python3.pkgs; [ setuptools ];
# nativeCheckInputs = with pkgs.python3.pkgs; [ pytestCheckHook ];
propagatedBuildInputs = with pkgs.python3.pkgs; [
django_4
djangorestframework
social-auth-core
social-auth-app-django
django-simple-history
django-htmx
];
installPhase = ''
mkdir -p $out/opt/forkosissuetracker
cp -r . $out/opt/forkosissuetracker
chmod +x $out/opt/forkosissuetracker/forkosissuetracker/manage.py
makeWrapper $out/opt/forkosissuetracker/forkosissuetracker/manage.py $out/bin/forkosissuetracker \
--prefix PYTHONPATH : "$PYTHONPATH"
'';
# pythonImportsCheck = [ "" ];
};
}

View file

@ -0,0 +1,16 @@
"""
ASGI config for forkosissuetracker project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'forkosissuetracker.settings')
application = get_asgi_application()

View file

@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand, CommandError
from issuetracker_app.models import
class Command(BaseCommand):
help = "Flush notifications from database"
def add_arguments(self, parser):
parser.add_argument("poll_ids", nargs="+", type=int)
def handle(self, *args, **options):
for poll_id in options["poll_ids"]:
try:
poll = Poll.objects.get(pk=poll_id)
except Poll.DoesNotExist:
raise CommandError('Poll "%s" does not exist' % poll_id)
poll.opened = False
poll.save()

View file

@ -0,0 +1,163 @@
"""
Django settings for forkosissuetracker project.
Generated by 'django-admin startproject' using Django 5.1.1.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-uingtjtoyh($w843$3@l8k*hwx3nep^4_fy5gtk0l)a5t(fsu1'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'social_django',
'simple_history',
'issuetracker_app',
'django_htmx',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
'django_htmx.middleware.HtmxMiddleware',
]
ROOT_URLCONF = 'forkosissuetracker.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
},
},
]
WSGI_APPLICATION = 'forkosissuetracker.wsgi.application'
SECURE_SSL_REDIRECT = False
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / "static"]
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20
}
# Django Auth
AUTHENTICATION_BACKENDS = [
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.github.GithubOAuth2',
'django.contrib.auth.backends.ModelBackend',
]
# Social Auth
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ['username', 'first_name', 'email']
SOCIAL_AUTH_LOGIN_URL = '/'
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/home/'
# TODO: replace with OIDC
SOCIAL_AUTH_GITHUB_KEY = ''
SOCIAL_AUTH_GITHUB_SECRET = ''

View file

@ -0,0 +1,40 @@
"""
URL configuration for forkosissuetracker project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.urls import include, path
from django.contrib import admin
from rest_framework import routers
from forkosissuetracker.views import login, home, logout
router = routers.DefaultRouter()
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
path('', login),
path('', include('social_django.urls', namespace='social')),
path('home/', home),
path('logout/', logout),
path('issues/', include('issuetracker_app.urls')),
# API
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path("admin/", admin.site.urls),
]

View file

@ -0,0 +1 @@
from .misc import *

View file

@ -0,0 +1,18 @@
from django.shortcuts import redirect, render
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.decorators import login_required
# from django.template.context import RequestContext
def login(request):
return render(request, 'login.html')
@login_required(login_url='/')
def home(request):
return render(request, 'home.html')
def logout(request):
auth_logout(request)
return redirect('/')

View file

@ -0,0 +1,16 @@
"""
WSGI config for forkosissuetracker project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'forkosissuetracker.settings')
application = get_wsgi_application()

View file

@ -0,0 +1,5 @@
from django.contrib import admin
from issuetracker_app.models import Comment, Issue
admin.site.register(Issue)
admin.site.register(Comment)

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class IssuetrackerAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'issuetracker_app'

View file

@ -0,0 +1,7 @@
from django import forms
from .models import Comment
class NewCommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['body']

View file

@ -0,0 +1,27 @@
from django.db import models
from simple_history.models import HistoricalRecords
from django.contrib.auth.models import User
class Issue(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=128)
description = models.CharField(max_length=2048)
last_modified = models.DateTimeField(auto_now=True)
history = HistoricalRecords()
def get_absolute_url(self):
return f"/issues/{self.id}"
def __str__(self):
return f"{self.title}"
class Meta:
ordering = ["last_modified"]
class Comment(models.Model):
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
body = models.CharField(max_length=2048)
last_modified = models.DateTimeField(auto_now=True)
history = HistoricalRecords()

View file

@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> -->
<title>ForkOS Issues</title>
</head>
<body>
{% load static %}
<script src="{% static 'htmx.min.js' %}" defer></script>
<div>
<h1 onclick="location.href='{% url 'index' %}'">
ForkOS Issues
</h1>
</div>
<div>
{% block content %}
This content will be replaced by different html code for each page.
{% endblock %}
</div>
</body>
</html>

View file

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block content %}
{% if object_list %}
<h3>All our issues</h3>
{% endif %}
<ul>
{% for issue in object_list %}
<li>
<div
role="button"
onclick="location.href='{% url "issue-detail" issue.id %}'">
{{ issue.title }} | {{ issue.author }}
</div>
</li>
{% empty %}
<h4>You have no lists!</h4>
{% endfor %}
</ul>
{% endblock %}

View file

@ -0,0 +1,38 @@
<h2>comment on the issue</h2>
<div id="comments_section">
{% if user.is_authenticated %}
<form method="POST">
{% csrf_token %}
<div class="form-group">
{{ comment_form }}
<button class="btn btn-info" type="submit">Add comment <i class="fas fa-comments"></i></button>
</div>
</form>
{% else %}
<a class="btn btn-outline-info" href="{% url 'login' %}?next={{request.path}}">Log in to add a comment!</a><br>
{% endif %}
{% if comments %}
<strong class="text-secondary">
{{ object.number_of_comments }} Comment{{ object.number_of_comments | pluralize }}
</strong>
<hr>
<ul>
{% for comment in comments %}
<li>
<div>
<span>
<strong class="text-info">{{ comment.author }} </strong>
<small class="text-muted">{{ comment.last_modified }}</small>
</span>
<p>
{{ comment.body | safe }}
</p>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<strong class="text-secondary">No comments yet...</strong>
{% endif %}
</div>

View file

@ -0,0 +1,4 @@
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Save">
</form>

View file

@ -0,0 +1,4 @@
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Update">
</form>

View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,25 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.ListIssueView.as_view(), name="index"),
# CRUD patterns for ToDoItems
path("add/",
views.IssueCreate.as_view(),
name="issue-add",
),
path(
"<int:pk>/",
# views.IssueUpdate.as_view(),
# name="issue-update",
views.IssueDetail.as_view(),
name="issue-detail",
),
]

View file

@ -0,0 +1,51 @@
from django.shortcuts import render
from django.views.generic import DetailView, ListView, CreateView, UpdateView
from .models import Comment, Issue
from .forms import NewCommentForm
class ListIssueView(ListView):
model = Issue
template_name = "./index.html"
class IssueCreate(CreateView):
model = Issue
fields = ["title", "description"]
template_name = "./issue_form.html"
def post(self, request, *args, **kwargs):
new_issue = Issue(
title=request.POST.get('title'),
description=request.POST.get('description'),
author=self.request.user
)
new_issue.save()
return self.get(self, request, *args, **kwargs)
class IssueUpdate(UpdateView):
model = Issue
fields = [
"title",
"description",
]
template_name = "./issue_update.html"
class IssueDetail(DetailView):
model = Issue
template_name = "./issue_detail.html"
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
comments_connected = Comment.objects.filter(issue = self.get_object()).order_by('-last_modified') # todo: change last_modified to the creation date
data['comments'] = comments_connected
if self.request.user.is_authenticated:
data['comment_form'] = NewCommentForm(instance=self.request.user
)
return data
def post(self, request, *args, **kwargs):
new_comment = Comment(body=request.POST.get('body'),
author=self.request.user,
issue=self.get_object())
new_comment.save()
return self.get(self, request, *args, **kwargs)

22
forkosissuetracker/manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'forkosissuetracker.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

65
forkosissuetracker/static/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
<h1>Welcome</h1>
<p><a href="/logout">Logout</a>

View file

@ -0,0 +1,8 @@
{% if user and not user.is_anonymous %}
<a>Hello, {{ user.get_full_name }}!</a>
<br>
<a href="/issues">Issues</a>
<a href="/logout">Logout</a>
{% else %}
<a href="{% url "social:begin" "github" %}">Login with github</a>
{% endif %}

80
npins/default.nix Normal file
View file

@ -0,0 +1,80 @@
# Generated by npins. Do not modify; will be overwritten regularly
let
data = builtins.fromJSON (builtins.readFile ./sources.json);
version = data.version;
mkSource =
spec:
assert spec ? type;
let
path =
if spec.type == "Git" then
mkGitSource spec
else if spec.type == "GitRelease" then
mkGitSource spec
else if spec.type == "PyPi" then
mkPyPiSource spec
else if spec.type == "Channel" then
mkChannelSource spec
else
builtins.throw "Unknown source type ${spec.type}";
in
spec // { outPath = path; };
mkGitSource =
{
repository,
revision,
url ? null,
hash,
branch ? null,
...
}:
assert repository ? type;
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
# In the latter case, there we will always be an url to the tarball
if url != null then
(builtins.fetchTarball {
inherit url;
sha256 = hash; # FIXME: check nix version & use SRI hashes
})
else
assert repository.type == "Git";
let
urlToName =
url: rev:
let
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
short = builtins.substring 0 7 rev;
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
in
"${if matched == null then "source" else builtins.head matched}${appendShort}";
name = urlToName repository.url revision;
in
builtins.fetchGit {
url = repository.url;
rev = revision;
inherit name;
# hash = hash;
};
mkPyPiSource =
{ url, hash, ... }:
builtins.fetchurl {
inherit url;
sha256 = hash;
};
mkChannelSource =
{ url, hash, ... }:
builtins.fetchTarball {
inherit url;
sha256 = hash;
};
in
if version == 3 then
builtins.mapAttrs (_: mkSource) data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"

11
npins/sources.json Normal file
View file

@ -0,0 +1,11 @@
{
"pins": {
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre674705.b833ff01a0d6/nixexprs.tar.xz",
"hash": "12cda9rvpgjcsxykbcg5cxjaayhibjjabv6svacjc5n5kpcbx5sf"
}
},
"version": 3
}

334
pdm.lock Normal file
View file

@ -0,0 +1,334 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:b80ed28bd741fd3fadad5fabd000d9da282328f90a0e97dfcd010b7006299768"
[[metadata.targets]]
requires_python = "==3.12.*"
[[package]]
name = "asgiref"
version = "3.8.1"
requires_python = ">=3.8"
summary = "ASGI specs, helper code, and adapters"
groups = ["default"]
dependencies = [
"typing-extensions>=4; python_version < \"3.11\"",
]
files = [
{file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
{file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
]
[[package]]
name = "certifi"
version = "2024.8.30"
requires_python = ">=3.6"
summary = "Python package for providing Mozilla's CA Bundle."
groups = ["default"]
files = [
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
]
[[package]]
name = "cffi"
version = "1.17.0"
requires_python = ">=3.8"
summary = "Foreign Function Interface for Python calling C code."
groups = ["default"]
marker = "platform_python_implementation != \"PyPy\""
dependencies = [
"pycparser",
]
files = [
{file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
{file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
{file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
{file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
{file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
{file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
{file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
]
[[package]]
name = "charset-normalizer"
version = "3.3.2"
requires_python = ">=3.7.0"
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
groups = ["default"]
files = [
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
]
[[package]]
name = "cryptography"
version = "43.0.1"
requires_python = ">=3.7"
summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
groups = ["default"]
dependencies = [
"cffi>=1.12; platform_python_implementation != \"PyPy\"",
]
files = [
{file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
{file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
{file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
{file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
{file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
{file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
{file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
{file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
{file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
{file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
{file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
{file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
]
[[package]]
name = "defusedxml"
version = "0.8.0rc2"
requires_python = ">=3.6"
summary = "XML bomb protection for Python stdlib modules"
groups = ["default"]
files = [
{file = "defusedxml-0.8.0rc2-py2.py3-none-any.whl", hash = "sha256:1c812964311154c3bf4aaf3bc1443b31ee13530b7f255eaaa062c0553c76103d"},
{file = "defusedxml-0.8.0rc2.tar.gz", hash = "sha256:138c7d540a78775182206c7c97fe65b246a2f40b29471e1a2f1b0da76e7a3942"},
]
[[package]]
name = "django"
version = "5.1.1"
requires_python = ">=3.10"
summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
groups = ["default"]
dependencies = [
"asgiref<4,>=3.8.1",
"sqlparse>=0.3.1",
"tzdata; sys_platform == \"win32\"",
]
files = [
{file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"},
{file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"},
]
[[package]]
name = "django-htmx"
version = "1.19.0"
requires_python = ">=3.8"
summary = "Extensions for using Django with htmx."
groups = ["default"]
dependencies = [
"asgiref>=3.6",
"django>=3.2",
]
files = [
{file = "django_htmx-1.19.0-py3-none-any.whl", hash = "sha256:875a642814e52278c1728842436beda2001847a493ab79fd82da3fb46ead140f"},
{file = "django_htmx-1.19.0.tar.gz", hash = "sha256:e7e17304e78e07f96eca0affc3ce1806edfdf3538bb7cb1912452b101f3e627d"},
]
[[package]]
name = "djangorestframework"
version = "3.15.2"
requires_python = ">=3.8"
summary = "Web APIs for Django, made easy."
groups = ["default"]
dependencies = [
"backports-zoneinfo; python_version < \"3.9\"",
"django>=4.2",
]
files = [
{file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"},
{file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"},
]
[[package]]
name = "idna"
version = "3.8"
requires_python = ">=3.6"
summary = "Internationalized Domain Names in Applications (IDNA)"
groups = ["default"]
files = [
{file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"},
{file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
]
[[package]]
name = "oauthlib"
version = "3.2.2"
requires_python = ">=3.6"
summary = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
groups = ["default"]
files = [
{file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
{file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
]
[[package]]
name = "pycparser"
version = "2.22"
requires_python = ">=3.8"
summary = "C parser in Python"
groups = ["default"]
marker = "platform_python_implementation != \"PyPy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
[[package]]
name = "pyjwt"
version = "2.9.0"
requires_python = ">=3.8"
summary = "JSON Web Token implementation in Python"
groups = ["default"]
files = [
{file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
]
[[package]]
name = "python3-openid"
version = "3.2.0"
summary = "OpenID support for modern servers and consumers."
groups = ["default"]
dependencies = [
"defusedxml",
]
files = [
{file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
{file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
]
[[package]]
name = "requests"
version = "2.32.3"
requires_python = ">=3.8"
summary = "Python HTTP for Humans."
groups = ["default"]
dependencies = [
"certifi>=2017.4.17",
"charset-normalizer<4,>=2",
"idna<4,>=2.5",
"urllib3<3,>=1.21.1",
]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[[package]]
name = "requests-oauthlib"
version = "2.0.0"
requires_python = ">=3.4"
summary = "OAuthlib authentication support for Requests."
groups = ["default"]
dependencies = [
"oauthlib>=3.0.0",
"requests>=2.0.0",
]
files = [
{file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"},
{file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
]
[[package]]
name = "social-auth-app-django"
version = "5.4.2"
requires_python = ">=3.8"
summary = "Python Social Authentication, Django integration."
groups = ["default"]
dependencies = [
"Django>=3.2",
"social-auth-core>=4.4.1",
]
files = [
{file = "social-auth-app-django-5.4.2.tar.gz", hash = "sha256:c8832c6cf13da6ad76f5613bcda2647d89ae7cfbc5217fadd13477a3406feaa8"},
{file = "social_auth_app_django-5.4.2-py3-none-any.whl", hash = "sha256:0c041a31707921aef9a930f143183c65d8c7b364381364a50f3f7c6fcc9d62f6"},
]
[[package]]
name = "social-auth-core"
version = "4.5.4"
requires_python = ">=3.8"
summary = "Python social authentication made simple."
groups = ["default"]
dependencies = [
"PyJWT>=2.7.0",
"cryptography>=1.4",
"defusedxml>=0.5.0rc1",
"oauthlib>=1.0.3",
"python3-openid>=3.0.10",
"requests-oauthlib>=0.6.1",
"requests>=2.9.1",
]
files = [
{file = "social-auth-core-4.5.4.tar.gz", hash = "sha256:d3dbeb0999ffd0e68aa4bd73f2ac698a18133fd11b3fc890e1366f18c8889fac"},
{file = "social_auth_core-4.5.4-py3-none-any.whl", hash = "sha256:33cf970a623c442376f9d4a86fb187579e4438649daa5b5be993d05e74d7b2db"},
]
[[package]]
name = "sqlparse"
version = "0.5.1"
requires_python = ">=3.8"
summary = "A non-validating SQL parser."
groups = ["default"]
files = [
{file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"},
{file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"},
]
[[package]]
name = "tzdata"
version = "2024.1"
requires_python = ">=2"
summary = "Provider of IANA time zone data"
groups = ["default"]
marker = "sys_platform == \"win32\""
files = [
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
[[package]]
name = "urllib3"
version = "2.2.2"
requires_python = ">=3.8"
summary = "HTTP library with thread-safe connection pooling, file post, and more."
groups = ["default"]
files = [
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
]

20
pyproject.toml Normal file
View file

@ -0,0 +1,20 @@
[project]
name = "forkos-issue-tracker"
version = "0.1.0"
description = "Default template for PDM package"
authors = [
{name = "Janik H.", email = "janik@aq0.de"},
]
dependencies = [
"djangorestframework>=3.15.2",
"social-auth-core>=4.5.4",
"social-auth-app-django>=5.4.2",
"django-htmx>=1.19.0",
]
requires-python = "==3.12.*"
readme = "README.md"
license = {text = "MIT"}
[tool.pdm]
distribution = false

1
shell.nix Normal file
View file

@ -0,0 +1 @@
(import ./default.nix {}).shell

0
tests/__init__.py Normal file
View file