diff --git a/client/next.config.mjs b/client/next.config.mjs index 8b6bf03..c8a0f71 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -1,12 +1,6 @@ -// import os from "node:os"; -// import isInsideContainer from "is-inside-container"; - -// const isWindowsDevContainer = () => -// os.release().toLowerCase().includes("microsoft") && isInsideContainer(); - /** @type {import('next').NextConfig} */ -const config = { +const nextConfig = { reactStrictMode: true, turbopack: { root: import.meta.dirname, @@ -14,14 +8,15 @@ const config = { outputFileTracingRoot: import.meta.dirname, images: { domains: ["localhost"], + remotePatterns: [ + { + protocol: 'http', + hostname: 'localhost', + port: '8000', + pathname: '/media/**', + }, + ], }, - // Turns on file change polling for the Windows Dev Container - // Doesn't work currently for turbopack, so file changes will not automatically update the client. - // watchOptions: isWindowsDevContainer() - // ? { - // pollIntervalMs: 1000 - // } - // : undefined, }; -export default config; +export default nextConfig; diff --git a/client/public/placeholder1293x405.svg b/client/public/placeholder1293x405.svg new file mode 100644 index 0000000..34f928c --- /dev/null +++ b/client/public/placeholder1293x405.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/components/ui/go-back-button.tsx b/client/src/components/ui/go-back-button.tsx new file mode 100644 index 0000000..5f1ebce --- /dev/null +++ b/client/src/components/ui/go-back-button.tsx @@ -0,0 +1,38 @@ +import Link from "next/link"; + +interface GoBackButtonProps { + url: string; + label: string; +} +const GoBackButton = ({ url, label }: GoBackButtonProps) => { + return ( + + + + ); +}; + +export default GoBackButton; diff --git a/client/src/components/ui/image-card.tsx b/client/src/components/ui/image-card.tsx new file mode 100644 index 0000000..f3ca51b --- /dev/null +++ b/client/src/components/ui/image-card.tsx @@ -0,0 +1,30 @@ +import Image from "next/image"; +import React from "react"; + +interface ImageCard { + imageSrc?: string; + imageAlt?: string; + children?: React.ReactNode; +} + +const ImageCard = ({ imageSrc, imageAlt = "Image", children }: ImageCard) => { + return ( +
+
+ {imageSrc ? ( + {imageAlt} + ) : ( + children || No Image + )} +
+
+ ); +}; + +export default ImageCard; diff --git a/client/src/components/ui/image-placeholder.tsx b/client/src/components/ui/image-placeholder.tsx new file mode 100644 index 0000000..b7e25e5 --- /dev/null +++ b/client/src/components/ui/image-placeholder.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +const ImagePlaceholder = () => { + return ( +
+
+ + + +
+
+ ); +}; +export default ImagePlaceholder; diff --git a/client/src/components/ui/modal/error-modal.tsx b/client/src/components/ui/modal/error-modal.tsx new file mode 100644 index 0000000..ba1c240 --- /dev/null +++ b/client/src/components/ui/modal/error-modal.tsx @@ -0,0 +1,45 @@ +import React, { useState } from "react"; + +interface ErrorModalProps { + message: string | null; + onClose: () => void; +} + +const ErrorModal = ({ message, onClose = () => {} }: ErrorModalProps) => { + const [isVisible, setIsVisible] = useState(true); + if (!isVisible || !message) { + return null; + } + + function onModalClose() { + setIsVisible(false); + onClose(); + } + + return ( + // Backdrop overlay +
+ {/* Modal content container */} +
e.stopPropagation()} // Prevent closing when clicking inside the modal + > +

Error

+

{message}

+
+ +
+
+
+ ); +}; + +export default ErrorModal; diff --git a/client/src/hooks/use-artwork-data.ts b/client/src/hooks/use-artwork-data.ts new file mode 100644 index 0000000..da08c9e --- /dev/null +++ b/client/src/hooks/use-artwork-data.ts @@ -0,0 +1,49 @@ +import { Art } from "@/types/art"; + +export const generateMockArtworks = (count: number): Art[] => { + const artworks: Art[] = []; + for (let i = 1; i <= count; i++) { + artworks.push({ + id: i, + name: `Artwork ${i}`, + description: "Mock artwork description", + //source_game: "Mock Game", + media: "", + active: true, + contributors: [], + //created_at: new Date().toISOString(), + }); + } + return artworks; +}; + +export const generateMockArtwork = (id: string): Art => { + return { + id: Number(id), + name: "Mock Artwork Title", + description: + "Lorem ipsum dolor sit amet. Non numquam dicta nam autem dicta 33 error molestias et repellat consequatur eum iste expedita est dolorem libero et quas provident!", + //source_game: "Mock Game", + media: "", + active: true, + //created_at: new Date().toISOString(), + contributors: [ + { + id: 1, + art_id: Number(id), + member_name: "Contributor 1", + role: "user1", + discord_url: "https://discord.com", + instagram_url: "", + }, + { + id: 2, + art_id: Number(id), + member_name: "Contributor 2", + role: "user2", + discord_url: "", + instagram_url: "https://instagram.com", + }, + ], + }; +}; diff --git a/client/src/pages/artwork/[id].tsx b/client/src/pages/artwork/[id].tsx new file mode 100644 index 0000000..3b2261e --- /dev/null +++ b/client/src/pages/artwork/[id].tsx @@ -0,0 +1,201 @@ +import { Instagram, MessageSquare } from "lucide-react"; +import { GetServerSideProps } from "next"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; + +import GoBackButton from "@/components/ui/go-back-button"; +import ImagePlaceholder from "@/components/ui/image-placeholder"; +import ErrorModal from "@/components/ui/modal/error-modal"; +import api from "@/lib/api"; +import { Art } from "@/types/art"; + +interface ArtworkPageProps { + artwork?: Art; + error?: string; +} + +function displayContributors(artwork: Art) { + return ( +
+
+
+
+ Contributors +
+
+
+ {artwork.contributors?.map((contributor) => ( +
+
+ {contributor.member_name} +
+
+ {contributor.discord_url && ( + + + + )} + {contributor.instagram_url && ( + + + + )} +
+
+ ))} +
+
+
+ ); +} + +export default function ArtworkPage({ artwork, error }: ArtworkPageProps) { + const router = useRouter(); + if (error) { + return router.back()} />; + } + return ( +
+
+
+ +
+
+
+
+ {artwork!.media ? ( + Artwork image + ) : ( + + )} +
+
+
+
+ {artwork!.name} +
+
+
+ + {artwork!.description} + +
+
+ {displayContributors(artwork!)} +
+
+
+
+
+ {artwork!.name} +
+
+
+ + {artwork!.description} + +
+
+ {displayContributors(artwork!)} +
+ +
+
+ Game Image +
+
+
+ TODO add footer +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps = async ( + context, +) => { + const { id } = context.params as { id: string }; + try { + const artResponse = await api.get(`arts/${id}`); + const artwork = artResponse.data; + return { props: { artwork } }; + } catch (err: unknown) { + return { + props: { error: (err as Error).message || "Failed to load artwork." }, + }; + } +}; diff --git a/client/src/pages/artwork/index.tsx b/client/src/pages/artwork/index.tsx new file mode 100644 index 0000000..4b765b7 --- /dev/null +++ b/client/src/pages/artwork/index.tsx @@ -0,0 +1,132 @@ +import { GetServerSideProps } from "next"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +import { Button } from "@/components/ui/button"; +import ImageCard from "@/components/ui/image-card"; +import ErrorModal from "@/components/ui/modal/error-modal"; +import { generateMockArtworks } from "@/hooks/use-artwork-data"; +import api from "@/lib/api"; +import { Art } from "@/types/art"; +import { PageResult } from "@/types/page-response"; + +interface ArtworksPageProps { + artworks?: PageResult; + error?: string; +} + +const PLACEHOLDER_ICON = ( +
+ + + +
+); + +function renderArtworkCard(artwork: Art) { + return ( + + + {!artwork.media && PLACEHOLDER_ICON} + + + ); +} + +export default function ArtworksPage({ artworks, error }: ArtworksPageProps) { + const router = useRouter(); + if (error) { + return router.back()} />; + } + return ( +
+
+
+ FEATURED +
+ SOME GAME +
+
+ {PLACEHOLDER_ICON} +
+
+ + + +
+
+ +
+
+ {artworks!.results.map((artwork) => renderArtworkCard(artwork))} +
+
+
+ TODO add footer +
+
+ ); +} + +export const getServerSideProps: GetServerSideProps< + ArtworksPageProps +> = async () => { + try { + const res = await api.get>("arts"); + return { props: { artworks: res.data } }; + //} catch (err: unknown) { + } catch { + // return { + // props: { error: (err as Error).message || "Failed to load artworks." }, + // }; + + // Fallback to mock data on error + const mockArtworks = generateMockArtworks(12); + return { + props: { + artworks: { + results: mockArtworks, + count: mockArtworks.length, + next: "", + previous: "", + }, + }, + }; + } +}; diff --git a/client/src/types/art-contributor.ts b/client/src/types/art-contributor.ts new file mode 100644 index 0000000..ed38150 --- /dev/null +++ b/client/src/types/art-contributor.ts @@ -0,0 +1,9 @@ +import { BaseDto } from "./base-dto"; + +export interface ArtContributor extends BaseDto { + art_id: number; + member_name: string; + role: string; + instagram_url?: string; // TODO [HanMinh] to refine where to get these info + discord_url?: string; +} diff --git a/client/src/types/art.ts b/client/src/types/art.ts new file mode 100644 index 0000000..f00c297 --- /dev/null +++ b/client/src/types/art.ts @@ -0,0 +1,10 @@ +import { ArtContributor } from "./art-contributor"; +import { BaseDto } from "./base-dto"; + +export interface Art extends BaseDto { + name: string; + description: string; + media: string; + active: boolean; + contributors: ArtContributor[]; +} diff --git a/client/src/types/base-dto.ts b/client/src/types/base-dto.ts new file mode 100644 index 0000000..9e3b687 --- /dev/null +++ b/client/src/types/base-dto.ts @@ -0,0 +1,3 @@ +export interface BaseDto { + id: number; +} diff --git a/client/src/types/page-response.ts b/client/src/types/page-response.ts new file mode 100644 index 0000000..e5fa692 --- /dev/null +++ b/client/src/types/page-response.ts @@ -0,0 +1,6 @@ +export interface PageResult { + count: number; + next: string; + previous: string; + results: T[]; +} diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 52306b8..9c4bd79 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -21,7 +21,8 @@ const config = { extend: { fontFamily: { sans: ["var(--font-sans)", ...fontFamily.sans], - jersey10: ["var(--font-jersey10)", ...fontFamily.sans], + jersey10: ["Jersey 10", ...fontFamily.sans], + dmSans: ["DM Sans", ...fontFamily.sans], firaCode: ["var(--font-firaCode)", ...fontFamily.sans], }, @@ -70,6 +71,12 @@ const config = { neutral_4: "var(--neutral-4)", light_1: "var(--light-1)", light_2: "var(--light-2)", + light_3: "var(--light-3)", + light_alt: "var(--light-alt)", + light_alt_2: "var(--light-alt-2)", + logo_blue_2: "var(--logo-blue-2)", + logo_blue_1: "var(--logo-blue-1)", + error: "var(--error)", }, borderRadius: { lg: "var(--radius)", diff --git a/server/game_dev/admin.py b/server/game_dev/admin.py index 46d358c..c2d8a5e 100644 --- a/server/game_dev/admin.py +++ b/server/game_dev/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Member, Event +from .models import Art, ArtContributor, Member, Event class MemberAdmin(admin.ModelAdmin): @@ -12,3 +12,5 @@ class EventAdmin(admin.ModelAdmin): admin.site.register(Member, MemberAdmin) admin.site.register(Event, EventAdmin) +admin.site.register(Art) +admin.site.register(ArtContributor) diff --git a/server/game_dev/migrations/0005_art_artcontributor.py b/server/game_dev/migrations/0005_art_artcontributor.py new file mode 100644 index 0000000..f3d0c90 --- /dev/null +++ b/server/game_dev/migrations/0005_art_artcontributor.py @@ -0,0 +1,68 @@ +# Generated by Django 5.1.14 on 2025-11-28 17:32 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0004_alter_event_date"), + ] + + operations = [ + migrations.CreateModel( + name="Art", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=200)), + ("description", models.CharField(max_length=200)), + ("path_to_media", models.CharField(max_length=500)), + ("active", models.BooleanField()), + ], + ), + migrations.CreateModel( + name="ArtContributor", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("role", models.CharField(max_length=100)), + ( + "art", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="contributors", + to="game_dev.art", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="art_contributions", + to="game_dev.member", + ), + ), + ], + options={ + "verbose_name": "Art Contributor", + "verbose_name_plural": "Art Contributors", + "unique_together": {("art", "member")}, + }, + ), + ] diff --git a/server/game_dev/migrations/0006_rename_path_to_media_to_media.py b/server/game_dev/migrations/0006_rename_path_to_media_to_media.py new file mode 100644 index 0000000..9571355 --- /dev/null +++ b/server/game_dev/migrations/0006_rename_path_to_media_to_media.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0005_art_artcontributor"), + ] + + operations = [ + # First, rename the field + migrations.RenameField( + model_name="art", + old_name="path_to_media", + new_name="media", + ), + # Then, alter the field to ImageField + migrations.AlterField( + model_name="art", + name="media", + field=models.ImageField(upload_to='art_images/'), + ), + ] diff --git a/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py b/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py new file mode 100644 index 0000000..3c917f6 --- /dev/null +++ b/server/game_dev/migrations/0007_alter_artcontributor_unique_together_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.15 on 2026-01-16 15:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("game_dev", "0006_rename_path_to_media_to_media"), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="artcontributor", + unique_together=set(), + ), + migrations.AlterField( + model_name="art", + name="active", + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name="art", + name="media", + field=models.ImageField(upload_to="art/"), + ), + migrations.AddConstraint( + model_name="artcontributor", + constraint=models.UniqueConstraint( + fields=("art", "member"), name="unique_art_member" + ), + ), + ] diff --git a/server/game_dev/models.py b/server/game_dev/models.py index 6398070..58c2e1f 100644 --- a/server/game_dev/models.py +++ b/server/game_dev/models.py @@ -22,3 +22,30 @@ class Event(models.Model): def __str__(self): return self.name + + +class Art(models.Model): + name = models.CharField(null=False, max_length=200) + description = models.CharField(max_length=200,) + # source_game = models.ForeignKey(Games, on_delete=models.CASCADE, related_name='art_pieces') #Need implement Games model + media = models.ImageField(upload_to='art/', null=False) + active = models.BooleanField(default=True) + + def __str__(self): + return str(self.name) + + +class ArtContributor(models.Model): + art = models.ForeignKey('Art', on_delete=models.CASCADE, related_name='contributors') + member = models.ForeignKey('Member', on_delete=models.CASCADE, related_name='art_contributions') + role = models.CharField(max_length=100) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['art', 'member'], name='unique_art_member') + ] + verbose_name = 'Art Contributor' + verbose_name_plural = 'Art Contributors' + + def __str__(self): + return f"{self.member.name} - {self.art.name} ({self.role})" diff --git a/server/game_dev/serializers.py b/server/game_dev/serializers.py index b50638d..d4715fa 100644 --- a/server/game_dev/serializers.py +++ b/server/game_dev/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Event, Member +from .models import Event, Art, ArtContributor, Member class EventSerializer(serializers.ModelSerializer): @@ -16,6 +16,23 @@ class Meta: ] +class ArtContributorSerializer(serializers.ModelSerializer): + member_name = serializers.CharField(source='member.name', read_only=True) + art_id = serializers.IntegerField(source='art.id', read_only=True) + + class Meta: + model = ArtContributor + fields = ['id', 'art_id', 'member', 'member_name', 'role'] + + +class ArtSerializer(serializers.ModelSerializer): + contributors = ArtContributorSerializer(many=True, read_only=True) + + class Meta: + model = Art + fields = ['id', 'name', 'description', 'media', 'active', 'contributors'] + + class MemberSerializer(serializers.ModelSerializer): class Meta: model = Member diff --git a/server/game_dev/urls.py b/server/game_dev/urls.py index e387e58..9fb36b6 100644 --- a/server/game_dev/urls.py +++ b/server/game_dev/urls.py @@ -1,6 +1,9 @@ from django.urls import path +from rest_framework.routers import DefaultRouter from .views import EventDetailAPIView +router = DefaultRouter() + urlpatterns = [ path("events//", EventDetailAPIView.as_view()), -] +] + router.urls diff --git a/server/game_dev/views.py b/server/game_dev/views.py index 71a747c..d38423c 100644 --- a/server/game_dev/views.py +++ b/server/game_dev/views.py @@ -2,8 +2,8 @@ # Create your views here. from rest_framework import generics -from .models import Event -from .serializers import EventSerializer +from .models import Event, Art +from .serializers import EventSerializer, ArtSerializer class EventDetailAPIView(generics.RetrieveAPIView): @@ -15,3 +15,14 @@ class EventDetailAPIView(generics.RetrieveAPIView): def get_queryset(self): return Event.objects.filter(id=self.kwargs["id"]) + + +class ArtDetailAPIView(generics.RetrieveAPIView): + """ + GET /api/artworks// + """ + serializer_class = ArtSerializer + lookup_url_kwarg = "id" + + def get_queryset(self): + return Art.objects.filter(id=self.kwargs["id"]) diff --git a/server/poetry.lock b/server/poetry.lock index 9e7859f..8e6deac 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "asgiref" @@ -6,7 +6,6 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -21,7 +20,6 @@ version = "2.15.8" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" -groups = ["dev"] files = [ {file = "astroid-2.15.8-py3-none-any.whl", hash = "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c"}, {file = "astroid-2.15.8.tar.gz", hash = "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a"}, @@ -37,8 +35,6 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -50,7 +46,6 @@ version = "5.1.15" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"}, {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"}, @@ -71,7 +66,6 @@ version = "4.4.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, @@ -87,7 +81,6 @@ version = "3.2.3" description = "Extensions for Django" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"}, {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, @@ -102,7 +95,6 @@ version = "3.15.2" description = "Web APIs for Django, made easy." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, @@ -117,7 +109,6 @@ version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" -groups = ["dev"] files = [ {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, @@ -134,7 +125,6 @@ version = "1.4" description = "Plugin to catch bad style specific to Django Projects." optional = false python-versions = ">=3.7.2,<4.0.0" -groups = ["dev"] files = [ {file = "flake8_django-1.4.tar.gz", hash = "sha256:4debba883084191568e3187416d1d6bdd4abd826da988f197a3c36572e9f30de"}, ] @@ -149,7 +139,6 @@ version = "1.5.1" description = "Let your Python tests travel through time" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, @@ -164,7 +153,6 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -186,7 +174,6 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" -groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -198,7 +185,6 @@ version = "1.10.0" description = "A fast and thorough lazy object proxy." optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "lazy-object-proxy-1.10.0.tar.gz", hash = "sha256:78247b6d45f43a52ef35c25b5581459e85117225408a4128a3daf8bf9648ac69"}, {file = "lazy_object_proxy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:855e068b0358ab916454464a884779c7ffa312b8925c6f7401e952dcf3b89977"}, @@ -245,7 +231,6 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" -groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -257,7 +242,6 @@ version = "24.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] files = [ {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, @@ -269,7 +253,6 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -385,7 +368,7 @@ fpx = ["olefile"] mic = ["olefile"] test-arrow = ["pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions ; python_version < \"3.10\""] +typing = ["typing-extensions"] xmp = ["defusedxml"] [[package]] @@ -394,7 +377,6 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -410,7 +392,6 @@ version = "2.11.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, @@ -422,7 +403,6 @@ version = "3.1.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, @@ -434,7 +414,6 @@ version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, @@ -455,7 +434,6 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -470,7 +448,6 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -485,7 +462,6 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["main"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -497,7 +473,6 @@ version = "0.5.1" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4"}, {file = "sqlparse-0.5.1.tar.gz", hash = "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e"}, @@ -513,8 +488,6 @@ version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] -markers = "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"}, @@ -526,7 +499,6 @@ version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.6" -groups = ["dev"] files = [ {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, @@ -601,6 +573,6 @@ files = [ ] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = "^3.12" content-hash = "f804c2f3998772b91e34ad214e5fcafe900bec97675f73046d3bcc79aba0f7db"