Basic game creating API (#2)

* Adding the base of our API

* Little file and lint adjustments

* Adding the lint command to Makefile

* Adding the Minesweeper logic for game creation

* Adding some tests for the Minesweeper algorithm

* Adding some tools command to Makefile like pre-commit and pip-tools

* Adding test help text to Makefile

* all new user is_staff=True, for development for now

* Now we can get the data from specific game

Adding game status

Adding game status

Fixing game models
This commit is contained in:
2020-11-05 23:29:35 -03:00
committed by GitHub
parent 55ae104806
commit 733f3e5992
35 changed files with 690 additions and 67 deletions

19
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,19 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/ambv/black
rev: stable
hooks:
- id: black
language_version: python3.8
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
hooks:
- id: flake8

View File

@@ -4,9 +4,14 @@ help:
@echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" @echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@echo " Available commands " @echo " Available commands "
@echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" @echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
@echo "⭐️ help : Show this message" @echo "⭐️ help : Show this message"
@echo "⭐️ clean : Removes all python cache and temporary files" @echo "⭐️ clean : Removes all python cache and temporary files"
@echo "⭐️ run : Runs the application using docker-compose" @echo "⭐️ run : Runs the application using docker-compose"
@echo "⭐️ lint : Lint the source using flake8 codestyle"
@echo "⭐️ test : Runs the tests using Docker"
@echo "⭐️ pre-commit-install : Install the pre-commit hook"
@echo "⭐️ pre-commit-run : Runs the standalone pre-commit routine for checking files"
@echo "⭐️ update-requirements : Using pip-compile(from pip-tools), update the requirements.txt with fixed version of used libraries"
@echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――" @echo "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――"
clean: clean:
@@ -18,3 +23,18 @@ clean:
run: run:
@docker-compose up @docker-compose up
lint:
@flake8 .
test: clean
@docker-compose run --rm app-test python manage.py test
pre-commit-install:
@pre-commit install
pre-commit-run:
@pre-commit run --all-files
update-requirements:
@pip-compile requirements.txt

View File

@@ -1,2 +1 @@
# Minesweeper (backend) # Minesweeper (backend)

0
api/__init__.py Normal file
View File

5
api/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = "api"

View File

30
api/resources/game.py Normal file
View File

@@ -0,0 +1,30 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from game.models import Game
from ..serializers import GameSerializer
class GameResource(APIView):
def post(self, request):
""" Creates a new game """
serializer = GameSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class GameSingleResource(APIView):
def get(self, request, game_id):
""" Returns a game serialized or not found """
try:
game = Game.objects.get(pk=game_id)
except Game.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
serializer = GameSerializer(game)
return Response(serializer.data, status=status.HTTP_200_OK)

8
api/resources/main.py Normal file
View File

@@ -0,0 +1,8 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
class MainResource(APIView):
def get(self, request):
return Response({"message": "Welcome to the game!"}, status=status.HTTP_200_OK)

View File

@@ -0,0 +1,4 @@
from .game import GameSerializer
__all__ = ["GameSerializer"]

9
api/serializers/game.py Normal file
View File

@@ -0,0 +1,9 @@
from rest_framework import serializers
from game.models import Game
class GameSerializer(serializers.ModelSerializer):
class Meta:
model = Game
fields = "__all__"

0
api/tests.py Normal file
View File

13
api/urls.py Normal file
View File

@@ -0,0 +1,13 @@
from django.urls import path
from .resources.main import MainResource
from .resources.game import GameResource, GameSingleResource
app_name = "api"
urlpatterns = [
path("games/<game_id>", GameSingleResource.as_view(), name="games_single"),
path("games", GameResource.as_view(), name="games"),
path("", MainResource.as_view(), name="main"),
]

View File

@@ -16,6 +16,7 @@ ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "127.0.0.1,localhost").split(",")
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
"rest_framework",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
@@ -23,6 +24,8 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"core", "core",
"api",
"game",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -68,9 +71,7 @@ DATABASES = {
"PASSWORD": os.getenv("DB_PASS"), "PASSWORD": os.getenv("DB_PASS"),
"HOST": os.getenv("DB_HOST"), "HOST": os.getenv("DB_HOST"),
"PORT": os.getenv("DB_PORT"), "PORT": os.getenv("DB_PORT"),
"OPTIONS": { "OPTIONS": {"charset": "utf8mb4"},
"charset": "utf8mb4",
},
} }
} }
@@ -80,17 +81,11 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", "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",
}, },
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
] ]
@@ -112,3 +107,12 @@ USE_TZ = True
# https://docs.djangoproject.com/en/3.1/howto/static-files/ # https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = "/static/" STATIC_URL = "/static/"
# Some Django Rest Framework parameters
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.TokenAuthentication"
],
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}

View File

@@ -1,21 +1,14 @@
"""app URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.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.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
from django.conf import settings
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("", include("api.urls")),
] ]
# We need this only for development purpose
if settings.DEBUG is True:
urlpatterns += [
path("admin/", admin.site.urls),
]

View File

@@ -2,4 +2,4 @@ from django.apps import AppConfig
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
name = 'core' name = "core"

View File

@@ -9,34 +9,90 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('auth', '0012_alter_user_first_name_max_length'), ("auth", "0012_alter_user_first_name_max_length"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='User', name="User",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('password', models.CharField(max_length=128, verbose_name='password')), "id",
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), models.AutoField(
('email', models.EmailField(max_length=254, unique=True, verbose_name='E-mail')), auto_created=True,
('first_name', models.CharField(blank=True, max_length=30, verbose_name='First name')), primary_key=True,
('last_name', models.CharField(blank=True, max_length=30, verbose_name='Last name')), serialize=False,
('date_joined', models.DateTimeField(auto_now_add=True, verbose_name='date joined')), verbose_name="ID",
('is_active', models.BooleanField(default=True, verbose_name='User active?')), ),
('is_staff', models.BooleanField(default=False, verbose_name='Staff?')), ),
('is_superuser', models.BooleanField(default=False, verbose_name='Superuser?')), ("password", models.CharField(max_length=128, verbose_name="password")),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), (
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), "last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"email",
models.EmailField(
max_length=254, unique=True, verbose_name="E-mail"
),
),
(
"first_name",
models.CharField(
blank=True, max_length=30, verbose_name="First name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=30, verbose_name="Last name"
),
),
(
"date_joined",
models.DateTimeField(auto_now_add=True, verbose_name="date joined"),
),
(
"is_active",
models.BooleanField(default=True, verbose_name="User active?"),
),
("is_staff", models.BooleanField(default=False, verbose_name="Staff?")),
(
"is_superuser",
models.BooleanField(default=False, verbose_name="Superuser?"),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A"
"user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
], ],
options={ options={
'verbose_name': 'User', "verbose_name": "User",
'verbose_name_plural': 'Users', "verbose_name_plural": "Users",
'db_table': 'users', "db_table": "users",
'ordering': ['first_name', 'last_name'], "ordering": ["first_name", "last_name"],
}, },
managers=[ managers=[("objects", core.managers.UserManager())],
('objects', core.managers.UserManager()),
],
), ),
] ]

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.1.3 on 2020-11-05 03:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("core", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="user",
name="groups",
field=models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. "
"A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2020-11-06 01:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0002_auto_20201105_0303"),
]
operations = [
migrations.AlterField(
model_name="user",
name="is_staff",
field=models.BooleanField(default=True, verbose_name="Staff?"),
),
]

View File

@@ -14,7 +14,7 @@ class User(AbstractBaseUser, PermissionsMixin):
date_joined = models.DateTimeField("date joined", auto_now_add=True) date_joined = models.DateTimeField("date joined", auto_now_add=True)
is_active = models.BooleanField("User active?", default=True) is_active = models.BooleanField("User active?", default=True)
is_staff = models.BooleanField("Staff?", default=False) is_staff = models.BooleanField("Staff?", default=True)
is_superuser = models.BooleanField("Superuser?", default=False) is_superuser = models.BooleanField("Superuser?", default=False)
objects = UserManager() objects = UserManager()

View File

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

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -18,6 +18,28 @@ services:
- 8000:8000 - 8000:8000
links: links:
- db - db
app-test:
build: .
container_name: mines-app-test
volumes:
- ./commands:/commands
- .:/app
environment:
- DB_USER=root
- DB_PASS=minesweeper
- DB_HOST=db
- DB_NAME=minesweeper
- DB_PORT=3306
- DEBUG=1
links:
- db-test
db-test:
image: mysql:8
container_name: mines-db-test
command: --default-authentication-plugin=mysql_native_password
environment:
- MYSQL_ROOT_PASSWORD=minesweeper
- MYSQL_DATABASE=minesweeper
db: db:
image: mysql:8 image: mysql:8
container_name: mines-db container_name: mines-db

0
game/__init__.py Normal file
View File

0
game/admin.py Normal file
View File

7
game/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class GameConfig(AppConfig):
name = "game"
verbose_name = "Game"
verbose_name_plural = "Games"

89
game/game.py Normal file
View File

@@ -0,0 +1,89 @@
import random
class Minesweeper:
board = []
def __init__(self, rows=10, cols=10, mines=5):
self.rows = rows
self.cols = cols
self.mines = mines
def create_board(self):
""" Creating the board cells with 0 as default value """
self.board = [[0 for col in range(self.cols)] for row in range(self.rows)]
def put_mine(self):
"""Put a single mine on the board.
The mine have a -1 value just for reference
"""
mine_position_row = random.randrange(0, self.rows)
mine_position_col = random.randrange(0, self.cols)
if self.is_mine(mine_position_row, mine_position_col):
self.put_mine()
self.board[mine_position_row][mine_position_col] = -1
return mine_position_row, mine_position_col
def put_mines(self):
""" Put the desired amount of mines on the board """
for mine in range(1, self.mines + 1):
mine_position_row, mine_position_col = self.put_mine()
self.create_mine_points(mine_position_row, mine_position_col)
def create_mine_points(self, mine_position_row, mine_position_col):
"""Populate the board with points that sorrounds the mine.
The reference used is the mine that was already placed"""
# North
self.increment_safe_point(mine_position_row - 1, mine_position_col)
# North-east
self.increment_safe_point(mine_position_row - 1, mine_position_col + 1)
# East
self.increment_safe_point(mine_position_row, mine_position_col + 1)
# South-east
self.increment_safe_point(mine_position_row + 1, mine_position_col + 1)
# South
self.increment_safe_point(mine_position_row + 1, mine_position_col)
# South-west
self.increment_safe_point(mine_position_row + 1, mine_position_col - 1)
# West
self.increment_safe_point(mine_position_row, mine_position_col - 1)
# North-west
self.increment_safe_point(mine_position_row - 1, mine_position_col - 1)
def is_mine(self, row, col):
""" Checks whether the given location is a mine or not """
try:
return self.board[row][col] == -1
except IndexError:
return False
def is_point_in_board(self, row, col):
""" Checks whether the location is inside board """
if row in range(0, self.rows) and col in range(0, self.cols):
return True
return False
def increment_safe_point(self, row, col):
""" Creates the mine's pontuation frame """
# Ignores if the point whether not in the board
if not self.is_point_in_board(row, col):
return
# Verify if the position have a mine on it
if self.is_mine(row, col):
return
# Increment the value of the position becaus is close to some mine
self.board[row][col] += 1

View File

@@ -0,0 +1,65 @@
# Generated by Django 3.1.3 on 2020-11-05 03:03
from django.db import migrations, models
import django_mysql.models
import internal.utils
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Game",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="Creation date"
),
),
(
"modified_at",
models.DateTimeField(auto_now=True, verbose_name="Last update"),
),
(
"rows",
models.PositiveIntegerField(default=10, verbose_name="Board rows"),
),
(
"cols",
models.PositiveIntegerField(default=10, verbose_name="Board cols"),
),
(
"mines",
models.PositiveIntegerField(
default=5, verbose_name="Mines on board"
),
),
(
"board",
django_mysql.models.JSONField(
default=internal.utils.empty_list,
verbose_name="Generated board",
),
),
],
options={
"verbose_name": "Game",
"verbose_name_plural": "Games",
"db_table": "games",
},
),
]

View File

@@ -0,0 +1,68 @@
# Generated by Django 3.1.3 on 2020-11-06 02:25
from django.db import migrations, models
import django_mysql.models
import game.models
import internal.utils
class Migration(migrations.Migration):
dependencies = [
("game", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="game",
name="status",
field=models.IntegerField(
choices=[(0, "NOT_PLAYED"), (1, "PLAYING"), (2, "FINISHED")],
default=game.models.GameStatuses["NOT_PLAYED"],
help_text="Actual game status",
),
),
migrations.AddField(
model_name="game",
name="win",
field=models.BooleanField(
blank=True,
default=None,
help_text="Did the user win the game?",
null=True,
verbose_name="Win?",
),
),
migrations.AlterField(
model_name="game",
name="board",
field=django_mysql.models.JSONField(
default=internal.utils.empty_list,
help_text="Whe generated board game",
verbose_name="Generated board",
),
),
migrations.AlterField(
model_name="game",
name="cols",
field=models.PositiveIntegerField(
default=10, help_text="Board's total columns", verbose_name="Board cols"
),
),
migrations.AlterField(
model_name="game",
name="mines",
field=models.PositiveIntegerField(
default=5,
help_text="Board's total placed mines",
verbose_name="Mines on board",
),
),
migrations.AlterField(
model_name="game",
name="rows",
field=models.PositiveIntegerField(
default=10, help_text="Board's total rows", verbose_name="Board rows"
),
),
]

View File

65
game/models.py Normal file
View File

@@ -0,0 +1,65 @@
from enum import IntEnum
from django.db import models
from django_mysql.models import JSONField
from internal.utils import empty_list
from .game import Minesweeper
class GameStatuses(IntEnum):
""" Enum was used as choices of Game.status because explicit is better than implicit """
NOT_PLAYED = 0
PLAYING = 1
FINISHED = 2
@classmethod
def choices(cls):
return [(key.value, key.name) for key in cls]
class Game(models.Model):
created_at = models.DateTimeField("Creation date", auto_now_add=True)
modified_at = models.DateTimeField("Last update", auto_now=True)
rows = models.PositiveIntegerField(
"Board rows", default=10, help_text="Board's total rows"
)
cols = models.PositiveIntegerField(
"Board cols", default=10, help_text="Board's total columns"
)
mines = models.PositiveIntegerField(
"Mines on board", default=5, help_text="Board's total placed mines"
)
board = JSONField(
"Generated board", default=empty_list, help_text="Whe generated board game"
)
win = models.BooleanField(
"Win?",
default=None,
null=True,
blank=True,
help_text="Did the user win the game?",
)
status = models.IntegerField(
choices=GameStatuses.choices(),
default=GameStatuses.NOT_PLAYED,
help_text="Actual game status",
)
def save(self, *args, **kwargs):
""" If the board was not defined, we create a new as default """
if not self.board:
ms = Minesweeper(self.rows, self.cols, self.mines)
ms.create_board()
ms.put_mines()
self.board = ms.board
super(Game, self).save(*args, **kwargs)
class Meta:
verbose_name = "Game"
verbose_name_plural = "Games"
db_table = "games"

0
game/tests/__init__.py Normal file
View File

86
game/tests/test_game.py Normal file
View File

@@ -0,0 +1,86 @@
from django.test import TestCase
from ..game import Minesweeper
class MinesweeperTestCase(TestCase):
def test_board_2x3x1(self):
ms = Minesweeper(2, 3)
ms.create_board()
expected_board = [[0, 0, 0], [0, 0, 0]]
self.assertEqual(ms.board, expected_board)
def test_the_board_should_have_8_mines(self):
expected_mines = 8
ms = Minesweeper(10, 10, expected_mines)
ms.create_board()
ms.put_mines()
located_mines = 0
for row in ms.board:
located_mines += row.count(-1)
self.assertEqual(located_mines, expected_mines)
def test_is_mine_on_0x1(self):
ms = Minesweeper(2, 2, 1)
ms.board = [[0, -1], [0, 0]]
self.assertEqual(ms.is_mine(0, 1), True)
def test_is_not_mine_on_1x1(self):
ms = Minesweeper(2, 2, 1)
ms.board = [[1, -1], [-1, 1]]
self.assertEqual(ms.is_mine(1, 1), False)
def test_is_point_is_not_on_the_board(self):
ms = Minesweeper(2, 2, 1)
ms.board = [[0, 0], [0, 0]]
self.assertEqual(ms.is_point_in_board(2, 1), False)
def test_is_point_is_on_the_board(self):
ms = Minesweeper(2, 2, 1)
ms.board = [[0, 0], [0, 0]]
self.assertEqual(ms.is_point_in_board(1, 1), True)
def test_pontuation_creation_around_one_mine(self):
ms = Minesweeper(3, 3, 1)
ms.board = [
[0, 0, 0],
[0, -1, 0],
[0, 0, 0],
]
expected_board = [
[1, 1, 1],
[1, -1, 1],
[1, 1, 1],
]
ms.create_mine_points(1, 1)
self.assertEqual(ms.board, expected_board)
def test_pontuation_creation_around_two_mines(self):
ms = Minesweeper(3, 3, 1)
ms.board = [
[0, 0, 0],
[0, -1, -1],
[0, 0, 0],
]
expected_board = [
[1, 2, 2],
[1, -1, -1],
[1, 2, 2],
]
ms.create_mine_points(1, 1)
ms.create_mine_points(1, 2)
self.assertEqual(ms.board, expected_board)

3
internal/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .utils import empty_list, empty_object
__all__ = ["empty_list", "empty_object"]

6
internal/utils.py Normal file
View File

@@ -0,0 +1,6 @@
def empty_object():
return {}
def empty_list():
return []

View File

@@ -4,33 +4,46 @@
# #
# pip-compile requirements.txt # pip-compile requirements.txt
# #
appdirs==1.4.4 # via -r requirements.txt, black appdirs==1.4.4 # via -r requirements.txt, black, virtualenv
asgiref==3.3.0 # via -r requirements.txt, django asgiref==3.3.0 # via -r requirements.txt, django
black==20.8b1 # via -r requirements.txt black==20.8b1 # via -r requirements.txt
cfgv==3.2.0 # via pre-commit
click==7.1.2 # via -r requirements.txt, black click==7.1.2 # via -r requirements.txt, black
distlib==0.3.1 # via virtualenv
django-dbml==0.3.5 # via -r requirements.txt django-dbml==0.3.5 # via -r requirements.txt
django-mysql==3.9.0 # via -r requirements.txt django-mysql==3.9.0 # via -r requirements.txt
django==3.1.3 # via -r requirements.txt, django-mysql, djangorestframework django==3.1.3 # via -r requirements.txt, django-mysql, djangorestframework
djangorestframework==3.12.1 # via -r requirements.txt djangorestframework==3.12.1 # via -r requirements.txt
filelock==3.0.12 # via virtualenv
flake8==3.8.4 # via -r requirements.txt
gevent==20.9.0 # via -r requirements.txt gevent==20.9.0 # via -r requirements.txt
greenlet==0.4.17 # via -r requirements.txt, gevent greenlet==0.4.17 # via -r requirements.txt, gevent
identify==1.5.9 # via pre-commit
importlib-metadata==2.0.0 # via -r requirements.txt, flake8, pre-commit, virtualenv
mccabe==0.6.1 # via -r requirements.txt, flake8
mypy-extensions==0.4.3 # via -r requirements.txt, black mypy-extensions==0.4.3 # via -r requirements.txt, black
mysqlclient==2.0.1 # via -r requirements.txt mysqlclient==2.0.1 # via -r requirements.txt
nodeenv==1.5.0 # via pre-commit
pathspec==0.8.0 # via -r requirements.txt, black pathspec==0.8.0 # via -r requirements.txt, black
pendulum==2.1.2 # via -r requirements.txt pendulum==2.1.2 # via -r requirements.txt
pre-commit==2.8.2 # via -r requirements.txt
pycodestyle==2.6.0 # via -r requirements.txt, flake8
pyflakes==2.2.0 # via -r requirements.txt, flake8
python-dateutil==2.8.1 # via -r requirements.txt, pendulum python-dateutil==2.8.1 # via -r requirements.txt, pendulum
pytz==2020.4 # via -r requirements.txt, django pytz==2020.4 # via -r requirements.txt, django
pytzdata==2020.1 # via -r requirements.txt, pendulum pytzdata==2020.1 # via -r requirements.txt, pendulum
pyyaml==5.3.1 # via pre-commit
regex==2020.10.28 # via -r requirements.txt, black regex==2020.10.28 # via -r requirements.txt, black
six==1.15.0 # via -r requirements.txt, python-dateutil six==1.15.0 # via -r requirements.txt, python-dateutil, virtualenv
sqlparse==0.4.1 # via -r requirements.txt, django sqlparse==0.4.1 # via -r requirements.txt, django
toml==0.10.2 # via -r requirements.txt, black toml==0.10.2 # via -r requirements.txt, black, pre-commit
typed-ast==1.4.1 # via -r requirements.txt, black typed-ast==1.4.1 # via -r requirements.txt, black
typing-extensions==3.7.4.3 # via -r requirements.txt, black typing-extensions==3.7.4.3 # via -r requirements.txt, black
uwsgi==2.0.19.1 # via -r requirements.txt uwsgi==2.0.19.1 # via -r requirements.txt
virtualenv==20.1.0 # via pre-commit
zipp==3.4.0 # via -r requirements.txt, importlib-metadata
zope.event==4.5.0 # via -r requirements.txt, gevent zope.event==4.5.0 # via -r requirements.txt, gevent
zope.interface==5.1.2 # via -r requirements.txt, gevent zope.interface==5.1.2 # via -r requirements.txt, gevent
flake8
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# setuptools # setuptools