mirror of
https://github.com/kennethreitz/photos.kennethreitz.org.git
synced 2026-06-05 06:46:13 +00:00
Initial project scaffold: Django 6 + uv, six app structure
ExifTree project with core, tree, gallery, groups, ingest, and search apps. Managed by uv with Django as the sole dependency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.so
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.venv/
|
||||
.env
|
||||
db.sqlite3
|
||||
*.sqlite3
|
||||
media/
|
||||
staticfiles/
|
||||
.DS_Store
|
||||
@@ -0,0 +1 @@
|
||||
3.14
|
||||
@@ -0,0 +1,94 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Project
|
||||
|
||||
ExifTree — a community platform where visual creators showcase work organized by the gear used to create it. Browse photography through cameras, lenses, and EXIF metadata.
|
||||
|
||||
**Domain:** exiftree.org
|
||||
**Stack:** Django 5.x · Python 3.12+ · PostgreSQL · Celery + Redis
|
||||
**Architecture doc:** `architecture.md`
|
||||
|
||||
## Code Style
|
||||
|
||||
- Python: follow PEP 8, use type hints on function signatures
|
||||
- Django: fat models, thin views — business logic lives on the model or in service functions, not in views
|
||||
- Imports: stdlib → third-party → django → local apps, separated by blank lines
|
||||
- Strings: double quotes for user-facing text, single quotes for identifiers and dict keys
|
||||
- Tests: use pytest + pytest-django, not unittest
|
||||
|
||||
## Project Layout
|
||||
|
||||
The Django project is created in the repo root (`django-admin startproject exiftree .`) — `manage.py` lives at the top level, not nested in a subdirectory.
|
||||
|
||||
## Django App Structure
|
||||
|
||||
The project is organized into focused Django apps:
|
||||
|
||||
- `core` — foundational models (User, Image, Camera, Lens, ExifData). Other apps import from here but never the reverse.
|
||||
- `tree` — browse-by-gear discovery pages. No models, reads from core.
|
||||
- `gallery` — user portfolios and collections.
|
||||
- `groups` — community spaces and memberships.
|
||||
- `ingest` — upload pipeline, EXIF extraction, thumbnail generation.
|
||||
- `search` — EXIF-powered filtering and search.
|
||||
|
||||
**Dependency rule:** `core` depends on nothing. All other apps may depend on `core`. Avoid cross-dependencies between feature apps — if two apps need to share logic, it probably belongs in `core`.
|
||||
|
||||
## Models
|
||||
|
||||
- Always use `UUIDField` for primary keys (not auto-increment integers)
|
||||
- Add `created_at` and `updated_at` timestamps to every model
|
||||
- Use `SlugField` on anything that appears in a URL
|
||||
- ExifData stores raw EXIF as a JSONField alongside parsed/indexed fields — never throw away the raw data
|
||||
- Camera and Lens records are canonical/normalized — raw EXIF strings map to these via the normalization layer in `core/normalization.py`
|
||||
|
||||
## EXIF Normalization
|
||||
|
||||
This is critical infrastructure. EXIF strings are inconsistent across manufacturers. The normalization pipeline must:
|
||||
|
||||
- Strip redundant manufacturer prefixes ("NIKON CORPORATION NIKON D850" → "Nikon", "D850")
|
||||
- Handle case normalization
|
||||
- Deduplicate via a lookup table of known aliases
|
||||
- Create new Camera/Lens records only when no match exists
|
||||
- Be idempotent — running normalization twice on the same input produces the same result
|
||||
|
||||
## Image Pipeline
|
||||
|
||||
Uploads flow through `ingest`:
|
||||
|
||||
1. Validate format and size
|
||||
2. Extract EXIF (Pillow / exifread)
|
||||
3. Normalize camera/lens → core models
|
||||
4. Generate thumbnails (small, medium, large)
|
||||
5. Store originals + thumbnails in object storage (Cloudflare R2)
|
||||
6. Create Image + ExifData records
|
||||
|
||||
All processing after initial validation happens async via Celery tasks. Never block the request/response cycle on image processing.
|
||||
|
||||
## Frontend
|
||||
|
||||
Django templates + HTMX for interactivity unless otherwise decided. Keep JavaScript minimal. The site should work without JS enabled for core browsing.
|
||||
|
||||
## Database
|
||||
|
||||
PostgreSQL. Key indexing priorities:
|
||||
|
||||
- ExifData: camera_id, lens_id, focal_length, aperture, iso, date_taken
|
||||
- Image: user_id, upload_date, visibility
|
||||
- Camera/Lens: slug, manufacturer
|
||||
|
||||
## URLs
|
||||
|
||||
- Camera tree: `/cameras/`, `/cameras/<manufacturer>/`, `/cameras/<manufacturer>/<model>/`
|
||||
- Lens tree: `/lenses/`, `/lenses/<manufacturer>/<model>/`
|
||||
- User profiles: `/@<username>/`
|
||||
- Collections: `/@<username>/collections/<slug>/`
|
||||
- Groups: `/groups/<slug>/`
|
||||
|
||||
## When Working on This Project
|
||||
|
||||
- Read `architecture.md` for full context on app structure and open decisions
|
||||
- Don't add dependencies without discussing tradeoffs first
|
||||
- Prefer Django's built-in tools over third-party packages when they're sufficient
|
||||
- Write migrations that are reversible
|
||||
- Keep the normalization lookup table in a format that's easy to contribute to (YAML or dict, not hardcoded if/else chains)
|
||||
- If a piece of logic could live in core or a feature app, default to the feature app — keep core minimal
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
# ExifTree — Architecture
|
||||
|
||||
**Domain:** exiftree.org
|
||||
**Stack:** Django · PostgreSQL · Python
|
||||
**Tagline:** Browse photography through the gear that made it.
|
||||
|
||||
---
|
||||
|
||||
## Concept
|
||||
|
||||
ExifTree is a community platform where visual creators showcase their work, organized around the gear used to create it. Click a camera name and see images from that camera across all users. Click a lens and browse the world through that glass.
|
||||
|
||||
EXIF metadata is the infrastructure — extracted automatically on upload, normalized into a browsable tree of cameras, lenses, and settings.
|
||||
|
||||
---
|
||||
|
||||
## Django Apps
|
||||
|
||||
### `core`
|
||||
|
||||
Owns the fundamental data models that everything else depends on. If you deleted every other app, `core` would still make sense on its own.
|
||||
|
||||
**Models:**
|
||||
|
||||
- `User` — extends AbstractUser. Username, email, bio, avatar, website URL, join date.
|
||||
- `Image` — the uploaded work. Title, description, file path, thumbnail paths, upload date, visibility (public/private/unlisted), view count. FK to User.
|
||||
- `Camera` — canonical camera record. Manufacturer, model, slug, display name. Normalized from raw EXIF strings.
|
||||
- `Lens` — canonical lens record. Manufacturer, model, slug, display name, focal length range, max aperture. Normalized from raw EXIF strings.
|
||||
- `ExifData` — one-to-one with Image. Raw EXIF blob (JSONField) plus parsed/indexed fields: FK to Camera, FK to Lens, focal length, aperture, shutter speed, ISO, date taken, GPS (nullable).
|
||||
|
||||
**Normalization layer:** EXIF strings are messy. "NIKON CORPORATION NIKON D850" and "Nikon D850" need to resolve to the same Camera record. Core owns a normalization pipeline — a lookup table of known aliases plus heuristics for splitting manufacturer/model. This can start as a simple mapping dict and evolve into something smarter over time.
|
||||
|
||||
---
|
||||
|
||||
### `tree`
|
||||
|
||||
The flagship feature. Browse-by-gear discovery pages.
|
||||
|
||||
**Routes:**
|
||||
|
||||
- `/cameras/` — grid of all cameras with image counts and sample thumbnails
|
||||
- `/cameras/<manufacturer>/` — all models from one brand
|
||||
- `/cameras/<manufacturer>/<model>/` — gallery of all images shot on this camera
|
||||
- `/lenses/` — same structure for lenses
|
||||
- `/lenses/<manufacturer>/<model>/`
|
||||
|
||||
**No new models.** Tree reads entirely from `core` models. It's a views/templates app that queries Camera, Lens, Image, and ExifData.
|
||||
|
||||
**Sorting/filtering within a camera page:** recent, most viewed, by lens, by focal length, by ISO range. All derived from ExifData fields.
|
||||
|
||||
---
|
||||
|
||||
### `gallery`
|
||||
|
||||
User-facing portfolios and collections.
|
||||
|
||||
**Models:**
|
||||
|
||||
- `Collection` — user-curated set of images. Title, description, cover image, ordering, visibility. FK to User.
|
||||
- `CollectionImage` — M2M through table. FK to Collection, FK to Image, sort order.
|
||||
|
||||
**Routes:**
|
||||
|
||||
- `/@<username>/` — user profile, shows their uploads
|
||||
- `/@<username>/collections/` — their collections
|
||||
- `/@<username>/collections/<slug>/` — single collection view
|
||||
|
||||
---
|
||||
|
||||
### `groups`
|
||||
|
||||
Community spaces. Not in core because the platform works without them.
|
||||
|
||||
**Models:**
|
||||
|
||||
- `Group` — name, slug, description, cover image, created date, visibility (public/private/invite-only).
|
||||
- `GroupMembership` — FK to User, FK to Group, role (member/moderator/admin), join date.
|
||||
- `GroupImage` — FK to Image, FK to Group, submitted date. An image can belong to multiple groups.
|
||||
|
||||
**Routes:**
|
||||
|
||||
- `/groups/` — browse/search groups
|
||||
- `/groups/<slug>/` — group page with member images
|
||||
- `/groups/<slug>/members/` — member list
|
||||
|
||||
**Open question:** Can groups be auto-generated from gear? e.g., "Fujifilm X100V Shooters" as a system group that anyone who uploads an X100V image is implicitly part of. Could blur the line between `tree` and `groups` in an interesting way.
|
||||
|
||||
---
|
||||
|
||||
### `ingest`
|
||||
|
||||
The upload and processing pipeline. Handles the heavy lifting so other apps don't have to.
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
1. Accept image upload (validate format, size limits)
|
||||
2. Extract EXIF via Pillow / exifread
|
||||
3. Run normalization — resolve Camera and Lens records
|
||||
4. Generate thumbnail variants (small, medium, large)
|
||||
5. Store original in object storage (S3/R2/B2)
|
||||
6. Store thumbnails in object storage
|
||||
7. Create Image + ExifData records in core
|
||||
|
||||
**Processing:** Celery task queue for async processing. Upload returns immediately, processing happens in background. Image marked as "processing" until complete.
|
||||
|
||||
**Bulk import:** Future feature — ingest from Lightroom catalogs, Flickr exports, or directories with EXIF intact.
|
||||
|
||||
---
|
||||
|
||||
### `search`
|
||||
|
||||
EXIF-powered search and filtering. Builds on top of core models.
|
||||
|
||||
**Capabilities:**
|
||||
|
||||
- Full-text search on image title/description
|
||||
- Filter by camera, lens, focal length range, aperture range, ISO range, date range
|
||||
- Filter by GPS/location (if present)
|
||||
- Combined queries: "all images shot on 85mm f/1.4 between ISO 100-400"
|
||||
|
||||
**Implementation:** Start with Django ORM queries + Postgres indexes. Move to Elasticsearch/Meilisearch if needed at scale.
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
### Image Storage
|
||||
|
||||
- **Originals:** Object storage (Cloudflare R2 preferred — no egress fees). Never serve originals directly.
|
||||
- **Thumbnails:** Generated on upload in 3 sizes. Served via CDN.
|
||||
- **CDN:** Cloudflare in front of R2. Images are the bulk of bandwidth.
|
||||
|
||||
### Database
|
||||
|
||||
PostgreSQL. Key indexes:
|
||||
|
||||
- ExifData: camera_id, lens_id, focal_length, aperture, iso, date_taken
|
||||
- Image: user_id, upload_date, visibility
|
||||
- Camera/Lens: slug, manufacturer
|
||||
|
||||
JSONField on ExifData stores the full raw EXIF blob for future use without migrations.
|
||||
|
||||
### Task Queue
|
||||
|
||||
Celery + Redis for async image processing. Tasks:
|
||||
|
||||
- EXIF extraction
|
||||
- Thumbnail generation
|
||||
- Camera/Lens normalization and dedup
|
||||
|
||||
### DNS / Hosting
|
||||
|
||||
- **DNS:** DNSimple (registered)
|
||||
- **App hosting:** TBD — Railway, Render, or DigitalOcean for MVP
|
||||
- **Object storage:** Cloudflare R2
|
||||
- **CDN:** Cloudflare
|
||||
|
||||
---
|
||||
|
||||
## Repo Structure
|
||||
|
||||
```
|
||||
exiftree/
|
||||
├── manage.py
|
||||
├── exiftree/
|
||||
│ ├── settings/
|
||||
│ │ ├── base.py
|
||||
│ │ ├── dev.py
|
||||
│ │ └── prod.py
|
||||
│ ├── urls.py
|
||||
│ └── wsgi.py
|
||||
├── core/
|
||||
│ ├── models.py
|
||||
│ ├── admin.py
|
||||
│ ├── normalization.py # EXIF string → canonical Camera/Lens
|
||||
│ ├── exif.py # extraction logic
|
||||
│ └── migrations/
|
||||
├── tree/
|
||||
│ ├── views.py
|
||||
│ ├── urls.py
|
||||
│ └── templates/tree/
|
||||
├── gallery/
|
||||
│ ├── models.py
|
||||
│ ├── views.py
|
||||
│ ├── urls.py
|
||||
│ └── templates/gallery/
|
||||
├── groups/
|
||||
│ ├── models.py
|
||||
│ ├── views.py
|
||||
│ ├── urls.py
|
||||
│ └── templates/groups/
|
||||
├── ingest/
|
||||
│ ├── tasks.py # Celery tasks
|
||||
│ ├── pipeline.py
|
||||
│ ├── views.py # upload endpoints
|
||||
│ └── urls.py
|
||||
├── search/
|
||||
│ ├── views.py
|
||||
│ ├── filters.py
|
||||
│ ├── urls.py
|
||||
│ └── templates/search/
|
||||
├── static/
|
||||
├── templates/ # base templates, shared partials
|
||||
├── pyproject.toml # uv-managed dependencies
|
||||
├── uv.lock
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Decisions
|
||||
|
||||
- **Frontend approach:** Django templates + HTMX for interactivity, or a separate SPA? HTMX keeps things simple and Pythonic. SPA gives richer interactions but doubles the stack.
|
||||
- **Auth:** Django built-in vs django-allauth for social login.
|
||||
- **Image formats:** Accept RAW files? Or JPEG/PNG/WebP only? RAW adds storage cost and processing complexity.
|
||||
- **Moderation:** Community platform needs content moderation. Manual review queue? AI-assisted? Report system?
|
||||
- **API:** REST API from day one (DRF) or add it later? Needed if a mobile app is ever on the table.
|
||||
- **Auto-groups from gear:** Should the tree pages and groups merge in some way? A camera page is basically a group without the social features.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = "core"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for exiftree 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/6.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exiftree.settings")
|
||||
|
||||
application = get_asgi_application()
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Django settings for exiftree project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 6.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# 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/6.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = "django-insecure-x8w4k=pjv*+&gl02p#ucsdh7*5)k&m7av=ehc8_6v756pg@f=j"
|
||||
|
||||
# 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",
|
||||
]
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "exiftree.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "exiftree.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/6.0/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/6.0/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/6.0/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
URL configuration for exiftree project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/6.0/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.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for exiftree 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/6.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "exiftree.settings")
|
||||
|
||||
application = get_wsgi_application()
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GalleryConfig(AppConfig):
|
||||
name = "gallery"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GroupsConfig(AppConfig):
|
||||
name = "groups"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IngestConfig(AppConfig):
|
||||
name = "ingest"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/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", "exiftree.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()
|
||||
@@ -0,0 +1,8 @@
|
||||
[project]
|
||||
name = "exiftree"
|
||||
version = "0.1.0"
|
||||
description = "Browse photography through the gear that made it."
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"django>=6.0.4",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SearchConfig(AppConfig):
|
||||
name = "search"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TreeConfig(AppConfig):
|
||||
name = "tree"
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -0,0 +1,55 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "6.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/b9/4155091ad1788b38563bd77a7258c0834e8c12a7f56f6975deaf54f8b61d/django-6.0.4.tar.gz", hash = "sha256:8cfa2572b3f2768b2e84983cf3c4811877a01edb64e817986ec5d60751c113ac", size = 10907407, upload-time = "2026-04-07T13:55:44.961Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/47/3d61d611609764aa71a37f7037b870e7bfb22937366974c4fd46cada7bab/django-6.0.4-py3-none-any.whl", hash = "sha256:14359c809fc16e8f81fd2b59d7d348e4d2d799da6840b10522b6edf7b8afc1da", size = 8368342, upload-time = "2026-04-07T13:55:37.999Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exiftree"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "django", specifier = ">=6.0.4" }]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2026.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user