mirror of
https://github.com/lemeow125/DRF_Template.git
synced 2025-09-18 05:29:37 +08:00
Switch to faker for test data
This commit is contained in:
parent
1e88f1ba53
commit
7e9501e75f
13 changed files with 190 additions and 126 deletions
|
@ -27,6 +27,7 @@ dependencies = [
|
||||||
"pytest-django>=4.11.1",
|
"pytest-django>=4.11.1",
|
||||||
"pytest-cov>=7.0.0",
|
"pytest-cov>=7.0.0",
|
||||||
"isort>=6.0.1",
|
"isort>=6.0.1",
|
||||||
|
"faker>=37.6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|
|
@ -5,7 +5,6 @@ Common model schemas
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.urls import reverse
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +20,3 @@ class CustomUser(AbstractUser):
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
return f"{self.first_name} {self.last_name}"
|
return f"{self.first_name} {self.last_name}"
|
||||||
|
|
||||||
@property
|
|
||||||
def admin_url(self):
|
|
||||||
return reverse("admin:users_customuser_change", args=(self.pk,))
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ class CustomUserSerializer(BaseUserSerializer):
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
|
||||||
class UserRegistrationSerializer(serializers.ModelSerializer):
|
class CustomUserRegistrationSerializer(serializers.ModelSerializer):
|
||||||
email = serializers.EmailField(required=True)
|
email = serializers.EmailField(required=True)
|
||||||
username = serializers.CharField(required=True)
|
username = serializers.CharField(required=True)
|
||||||
password = serializers.CharField(
|
password = serializers.CharField(
|
||||||
|
|
|
@ -8,7 +8,9 @@ from django.contrib.auth.tokens import default_token_generator
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from djoser.conf import settings
|
from djoser.conf import settings
|
||||||
from djoser.views import UserViewSet as DjoserUserViewSet
|
from djoser.views import UserViewSet as DjoserUserViewSet
|
||||||
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
|
@ -23,6 +25,9 @@ class CustomUserViewSet(DjoserUserViewSet):
|
||||||
token_generator = default_token_generator
|
token_generator = default_token_generator
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
"""
|
||||||
|
Overriden class function for injection of cache invalidation
|
||||||
|
"""
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
if user.is_superuser:
|
if user.is_superuser:
|
||||||
|
@ -43,6 +48,9 @@ class CustomUserViewSet(DjoserUserViewSet):
|
||||||
return CustomUser.objects.none()
|
return CustomUser.objects.none()
|
||||||
|
|
||||||
def perform_update(self, serializer, *args, **kwargs):
|
def perform_update(self, serializer, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Overriden class function for injection of cache invalidation
|
||||||
|
"""
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
super().perform_update(serializer, *args, **kwargs)
|
super().perform_update(serializer, *args, **kwargs)
|
||||||
|
@ -51,6 +59,9 @@ class CustomUserViewSet(DjoserUserViewSet):
|
||||||
cache.delete(f"users:{user.id}")
|
cache.delete(f"users:{user.id}")
|
||||||
|
|
||||||
def perform_create(self, serializer, *args, **kwargs):
|
def perform_create(self, serializer, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Overriden class function for injection of cache invalidation
|
||||||
|
"""
|
||||||
user = serializer.save(*args, **kwargs)
|
user = serializer.save(*args, **kwargs)
|
||||||
|
|
||||||
# Try-catch block for email sending
|
# Try-catch block for email sending
|
||||||
|
@ -70,9 +81,14 @@ class CustomUserViewSet(DjoserUserViewSet):
|
||||||
methods=["post"], detail=False, url_path="activation", url_name="activation"
|
methods=["post"], detail=False, url_path="activation", url_name="activation"
|
||||||
)
|
)
|
||||||
def activation(self, request, *args, **kwargs):
|
def activation(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Overriden class function for injection of cache invalidation
|
||||||
|
"""
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
super().activation(request, *args, **kwargs)
|
super().activation(request, *args, **kwargs)
|
||||||
|
|
||||||
# Clear cache
|
# Clear cache
|
||||||
cache.delete("users")
|
cache.delete("users")
|
||||||
cache.delete(f"users:{user.id}")
|
cache.delete(f"users:{user.id}")
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from drf_spectacular.views import (SpectacularAPIView, SpectacularRedocView,
|
from drf_spectacular.views import (
|
||||||
SpectacularSwaggerView)
|
SpectacularAPIView,
|
||||||
|
SpectacularRedocView,
|
||||||
|
SpectacularSwaggerView,
|
||||||
|
)
|
||||||
|
|
||||||
from core.settings import config
|
from core.settings import config
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,14 @@ import re
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import (BaseModel, EmailStr, Field, StrictStr, field_validator,
|
from pydantic import (
|
||||||
model_validator)
|
BaseModel,
|
||||||
|
EmailStr,
|
||||||
|
Field,
|
||||||
|
StrictStr,
|
||||||
|
field_validator,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
from pydantic_extra_types.timezone_name import TimeZoneName
|
from pydantic_extra_types.timezone_name import TimeZoneName
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,9 +68,17 @@ class Config(BaseModel):
|
||||||
REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field(
|
REFRESH_TOKEN_LIFETIME_DAYS: timedelta = Field(
|
||||||
default=timedelta(days=3), description="Refresh token lifetime in days"
|
default=timedelta(days=3), description="Refresh token lifetime in days"
|
||||||
)
|
)
|
||||||
|
DEBUG_USER_EMAIL: StrictStr = Field(
|
||||||
|
json_schema_extra={"required": DEBUG},
|
||||||
|
description="Administrator user email used for development",
|
||||||
|
)
|
||||||
|
DEBUG_USER_USERNAME: StrictStr = Field(
|
||||||
|
json_schema_extra={"required": DEBUG},
|
||||||
|
description="Administrator username used for development",
|
||||||
|
)
|
||||||
DEBUG_USER_PASSWORD: StrictStr = Field(
|
DEBUG_USER_PASSWORD: StrictStr = Field(
|
||||||
json_schema_extra={"required": True},
|
json_schema_extra={"required": DEBUG},
|
||||||
description="Password for test users created during development",
|
description="Password for administrator and test users created during development",
|
||||||
)
|
)
|
||||||
CACHE_USERNAME: StrictStr = Field(
|
CACHE_USERNAME: StrictStr = Field(
|
||||||
json_schema_extra={"required": True},
|
json_schema_extra={"required": True},
|
||||||
|
|
|
@ -8,7 +8,7 @@ from core.settings import * # noqa: F403
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.sqlite3",
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
"NAME": BASE_DIR / "test_db.sqlite3", # noqa: F405
|
"NAME": BASE_DIR / "db.sqlite3", # noqa: F405
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,71 +2,89 @@
|
||||||
Post-migrate signal handlers for creating initial data for accounts app.
|
Post-migrate signal handlers for creating initial data for accounts app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
from faker import Faker
|
||||||
|
|
||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
from core.settings import TESTS_DIR, config
|
from core.settings import config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
def get_users_json():
|
def generate_random_user(active: bool = False):
|
||||||
"""
|
"""
|
||||||
Function to read test user data from JSON file
|
Function to generate a single random user
|
||||||
|
|
||||||
|
Args:
|
||||||
|
active (bool, optional): User active status. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CustomUser: Newly created USER instance
|
||||||
"""
|
"""
|
||||||
with open(os.path.join(TESTS_DIR, "users", "users.json"), "r") as f:
|
USER = CustomUser.objects.create_user(
|
||||||
# Load JSON data
|
username=fake.user_name(),
|
||||||
data = json.loads(f.read())
|
email=fake.email(),
|
||||||
return data
|
password=config.DEBUG_USER_PASSWORD,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Additional user fields not covered by create() method
|
||||||
|
USER.first_name = fake.first_name()
|
||||||
|
USER.last_name = fake.last_name()
|
||||||
|
USER.is_active = active
|
||||||
|
USER.save()
|
||||||
|
|
||||||
|
return USER
|
||||||
|
|
||||||
|
|
||||||
def generate_test_users():
|
def generate_superuser():
|
||||||
"""
|
"""
|
||||||
Function to create test users.
|
Function to generate a test superuser account
|
||||||
"""
|
"""
|
||||||
data = get_users_json()
|
USER = CustomUser.objects.filter(is_superuser=True).first()
|
||||||
|
if not USER:
|
||||||
for user in data["users"]:
|
CustomUser.objects.create_superuser(
|
||||||
# Check if user already exists
|
username=config.DEBUG_USER_USERNAME,
|
||||||
USER = CustomUser.objects.filter(username=user["username"]).first()
|
email=config.DEBUG_USER_EMAIL,
|
||||||
if not USER:
|
password=config.DEBUG_USER_PASSWORD,
|
||||||
# Create user
|
)
|
||||||
if user["is_superuser"]:
|
return USER
|
||||||
USER = CustomUser.objects.create_superuser(
|
|
||||||
username=user["username"],
|
|
||||||
email=user["email"],
|
|
||||||
password=config.DEBUG_USER_PASSWORD,
|
|
||||||
)
|
|
||||||
logger.info("Created Superuser:", user["username"])
|
|
||||||
else:
|
|
||||||
USER = CustomUser.objects.create_user(
|
|
||||||
username=user["username"],
|
|
||||||
email=user["email"],
|
|
||||||
password=config.DEBUG_USER_PASSWORD,
|
|
||||||
)
|
|
||||||
logger.info("Created User:", user["username"])
|
|
||||||
|
|
||||||
# Additional user fields not covered by create() methods
|
|
||||||
USER.first_name = user["first_name"]
|
|
||||||
USER.last_name = user["last_name"]
|
|
||||||
USER.is_active = True
|
|
||||||
USER.save()
|
|
||||||
|
|
||||||
|
|
||||||
def remove_test_users():
|
def generate_test_users(count=3, active: bool = False) -> list[CustomUser]:
|
||||||
"""
|
"""
|
||||||
Function to remove test users in DEBUG mode.
|
Function to generate a list of test users.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count (int, optional): Number of regular test users to generate. Defaults to 10.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[CustomUser]: List containing the test superuser and generated regular users.
|
||||||
"""
|
"""
|
||||||
if config.DEBUG:
|
USERS = []
|
||||||
data = get_users_json()
|
|
||||||
for user in data["users"]:
|
# Superuser
|
||||||
# Check if user already exists
|
USERS.append(generate_superuser())
|
||||||
USER = CustomUser.objects.filter(username=user["username"]).first()
|
|
||||||
if USER:
|
# Regular users
|
||||||
USER.delete()
|
USERS.extend([generate_random_user(active=active) for _ in range(count)])
|
||||||
else:
|
|
||||||
logger.warning(
|
return USERS
|
||||||
f"Skipping user deletion for {user['username']}: Does not exist"
|
|
||||||
)
|
|
||||||
|
def remove_test_users(
|
||||||
|
USERS: list[CustomUser] = CustomUser.objects.filter(
|
||||||
|
is_superuser=False),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Function to remove test users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for USER in USERS:
|
||||||
|
USER = CustomUser.objects.filter(username=USER.username).first()
|
||||||
|
if USER:
|
||||||
|
USER.delete()
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Skipping user deletion for {USER.username}: Does not exist"
|
||||||
|
)
|
||||||
|
|
32
src/tests/users/test_user_activation.py
Normal file
32
src/tests/users/test_user_activation.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import pytest
|
||||||
|
import users
|
||||||
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from djoser.utils import encode_uid
|
||||||
|
|
||||||
|
from accounts.views import CustomUserViewSet
|
||||||
|
|
||||||
|
factory = RequestFactory()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db(transaction=True)
|
||||||
|
def test_user_activation():
|
||||||
|
"""
|
||||||
|
Test activation
|
||||||
|
"""
|
||||||
|
# Generate test user
|
||||||
|
USER = users.generate_random_user(active=False)
|
||||||
|
|
||||||
|
uid = encode_uid(USER.pk)
|
||||||
|
token = default_token_generator.make_token(USER)
|
||||||
|
|
||||||
|
view = CustomUserViewSet.as_view(actions={"post": "activation"})
|
||||||
|
|
||||||
|
request = factory.post(
|
||||||
|
"/accounts/users/activation/", data={"uid": uid, "token": token}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = view(request)
|
||||||
|
response.render()
|
||||||
|
|
||||||
|
assert response.status_code == 204
|
|
@ -1,38 +1,45 @@
|
||||||
import pytest
|
import pytest
|
||||||
import users
|
|
||||||
|
|
||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
|
from users import generate_test_users, remove_test_users
|
||||||
|
|
||||||
|
|
||||||
def assert_users_created():
|
def assert_users_exist(USERS: list[CustomUser] = []):
|
||||||
data = users.get_users_json()
|
"""
|
||||||
|
Asserts that each user in the provided list exists in the database.
|
||||||
|
|
||||||
for user in data["users"]:
|
Args:
|
||||||
USER = CustomUser.objects.filter(username=user["username"]).first()
|
USERS (list[CustomUser], optional): A list of CustomUser instances to check for existence. Defaults to an empty list.
|
||||||
|
|
||||||
# Assert user exists
|
Raises:
|
||||||
assert USER
|
AssertionError: If any user in the list does not exist in the database.
|
||||||
|
"""
|
||||||
|
|
||||||
if user["is_superuser"]:
|
for USER in USERS:
|
||||||
# Assert is superuser
|
assert CustomUser.objects.filter(username=USER.username).first()
|
||||||
assert USER.is_superuser
|
|
||||||
|
|
||||||
|
|
||||||
def assert_users_removed():
|
def assert_users_removed(USERS: list[CustomUser] = []):
|
||||||
data = users.get_users_json()
|
"""
|
||||||
for user in data["users"]:
|
Asserts that the specified users have been removed from the database.
|
||||||
USER = CustomUser.objects.filter(username=user["username"]).first()
|
|
||||||
|
|
||||||
# Assert user does not exist
|
Args:
|
||||||
assert not USER
|
USERS (list[CustomUser], optional): A list of user objects (dictionaries) containing at least the 'username' key.
|
||||||
|
Defaults to an empty list.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AssertionError: If any user in the USERS list still exists in the database.
|
||||||
|
"""
|
||||||
|
for USER in USERS:
|
||||||
|
assert not CustomUser.objects.filter(username=USER.username).first()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db(transaction=True)
|
@pytest.mark.django_db(transaction=True)
|
||||||
def test_user_creation_deletion():
|
def test_user_creation_deletion():
|
||||||
"""
|
"""
|
||||||
Test user creation and deletion
|
Test multiple instances of user creations and deletions
|
||||||
"""
|
"""
|
||||||
users.generate_test_users()
|
USERS = generate_test_users()
|
||||||
assert_users_created()
|
assert_users_exist(USERS)
|
||||||
users.remove_test_users()
|
remove_test_users(USERS)
|
||||||
assert_users_removed()
|
assert_users_removed(USERS)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
import users
|
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
from users import generate_test_users
|
||||||
|
|
||||||
from core.settings import config
|
from core.settings import config
|
||||||
|
|
||||||
|
@ -10,19 +10,15 @@ client = APIClient()
|
||||||
@pytest.mark.django_db(transaction=True)
|
@pytest.mark.django_db(transaction=True)
|
||||||
def test_user_login():
|
def test_user_login():
|
||||||
"""
|
"""
|
||||||
Test login
|
Test login with multiple user accounts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = users.get_users_json()
|
USERS = generate_test_users(active=True)
|
||||||
|
|
||||||
# Generate test users
|
for USER in USERS:
|
||||||
users.generate_test_users()
|
|
||||||
|
|
||||||
for user in data["users"]:
|
|
||||||
login_response = client.post(
|
login_response = client.post(
|
||||||
"/api/v1/accounts/jwt/create/",
|
"/api/v1/accounts/jwt/create/",
|
||||||
{"username": user["username"],
|
{"username": USER.username, "password": config.DEBUG_USER_PASSWORD},
|
||||||
"password": config.DEBUG_USER_PASSWORD},
|
|
||||||
format="json",
|
format="json",
|
||||||
).json()
|
).json()
|
||||||
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
{
|
|
||||||
"users": [
|
|
||||||
{
|
|
||||||
"username": "admin",
|
|
||||||
"email": "admin@test.com",
|
|
||||||
"is_superuser": true,
|
|
||||||
"first_name": "Test",
|
|
||||||
"last_name": "Admin"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "testuser1",
|
|
||||||
"email": "user1@test.com",
|
|
||||||
"is_superuser": false,
|
|
||||||
"first_name": "Test",
|
|
||||||
"last_name": "User 1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "testuser2",
|
|
||||||
"email": "user2@test.com",
|
|
||||||
"is_superuser": false,
|
|
||||||
"first_name": "Test",
|
|
||||||
"last_name": "User 2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "testuser3",
|
|
||||||
"email": "user3@test.com",
|
|
||||||
"is_superuser": false,
|
|
||||||
"first_name": "Test",
|
|
||||||
"last_name": "User 3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
14
uv.lock
generated
14
uv.lock
generated
|
@ -432,6 +432,7 @@ dependencies = [
|
||||||
{ name = "djoser" },
|
{ name = "djoser" },
|
||||||
{ name = "drf-spectacular" },
|
{ name = "drf-spectacular" },
|
||||||
{ name = "drf-spectacular-sidecar" },
|
{ name = "drf-spectacular-sidecar" },
|
||||||
|
{ name = "faker" },
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "isort" },
|
{ name = "isort" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
|
@ -458,6 +459,7 @@ requires-dist = [
|
||||||
{ name = "djoser", specifier = ">=2.3.3" },
|
{ name = "djoser", specifier = ">=2.3.3" },
|
||||||
{ name = "drf-spectacular", specifier = ">=0.28.0" },
|
{ name = "drf-spectacular", specifier = ">=0.28.0" },
|
||||||
{ name = "drf-spectacular-sidecar", specifier = ">=2025.8.1" },
|
{ name = "drf-spectacular-sidecar", specifier = ">=2025.8.1" },
|
||||||
|
{ name = "faker", specifier = ">=37.6.0" },
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
{ name = "isort", specifier = ">=6.0.1" },
|
{ name = "isort", specifier = ">=6.0.1" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
|
||||||
|
@ -483,6 +485,18 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
|
{ url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "faker"
|
||||||
|
version = "37.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/24/cd/f7679c20f07d9e2013123b7f7e13809a3450a18d938d58e86081a486ea15/faker-37.6.0.tar.gz", hash = "sha256:0f8cc34f30095184adf87c3c24c45b38b33ad81c35ef6eb0a3118f301143012c", size = 1907960, upload-time = "2025-08-26T15:56:27.419Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/7d/8b50e4ac772719777be33661f4bde320793400a706f5eb214e4de46f093c/faker-37.6.0-py3-none-any.whl", hash = "sha256:3c5209b23d7049d596a51db5d76403a0ccfea6fc294ffa2ecfef6a8843b1e6a7", size = 1949837, upload-time = "2025-08-26T15:56:25.33Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gprof2dot"
|
name = "gprof2dot"
|
||||||
version = "2025.4.14"
|
version = "2025.4.14"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue