This commit is contained in:
Ruidy Nemausat 2020-07-29 23:41:16 +02:00
commit 35816b96c2
21 changed files with 2230 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
**/api/migrations
**/api/data/.ipynb_checkpoints
**/api/data/*csv
db*

17
Pipfile Normal file
View file

@ -0,0 +1,17 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
pandas = "*"
djangorestframework = "*"
django = "*"
sklearn = "*"
numpy = "*"
joblib = "*"
[requires]
python_version = "3.7"

190
Pipfile.lock generated Normal file
View file

@ -0,0 +1,190 @@
{
"_meta": {
"hash": {
"sha256": "b61f243cbc1d68a628203e9bf1210e97d98d5e74516535f2cb22fccf2471043b"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"asgiref": {
"hashes": [
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
],
"version": "==3.2.10"
},
"django": {
"hashes": [
"sha256:31a5fbbea5fc71c99e288ec0b2f00302a0a92c44b13ede80b73a6a4d6d205582",
"sha256:5457fc953ec560c5521b41fad9e6734a4668b7ba205832191bbdff40ec61073c"
],
"index": "pypi",
"version": "==3.0.8"
},
"djangorestframework": {
"hashes": [
"sha256:05809fc66e1c997fd9a32ea5730d9f4ba28b109b9da71fccfa5ff241201fd0a4",
"sha256:e782087823c47a26826ee5b6fa0c542968219263fb3976ec3c31edab23a4001f"
],
"index": "pypi",
"version": "==3.11.0"
},
"joblib": {
"hashes": [
"sha256:8f52bf24c64b608bf0b2563e0e47d6fcf516abc8cfafe10cfd98ad66d94f92d6",
"sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49"
],
"index": "pypi",
"version": "==0.16.0"
},
"numpy": {
"hashes": [
"sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983",
"sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065",
"sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968",
"sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132",
"sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129",
"sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff",
"sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93",
"sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a",
"sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7",
"sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd",
"sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055",
"sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc",
"sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7",
"sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624",
"sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b",
"sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69",
"sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491",
"sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954",
"sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72",
"sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7",
"sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae",
"sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1",
"sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a",
"sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e",
"sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e",
"sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"
],
"index": "pypi",
"version": "==1.19.1"
},
"pandas": {
"hashes": [
"sha256:0210f8fe19c2667a3817adb6de2c4fd92b1b78e1975ca60c0efa908e0985cbdb",
"sha256:0227e3a6e3a22c0e283a5041f1e3064d78fbde811217668bb966ed05386d8a7e",
"sha256:0bc440493cf9dc5b36d5d46bbd5508f6547ba68b02a28234cd8e81fdce42744d",
"sha256:16504f915f1ae424052f1e9b7cd2d01786f098fbb00fa4e0f69d42b22952d798",
"sha256:182a5aeae319df391c3df4740bb17d5300dcd78034b17732c12e62e6dd79e4a4",
"sha256:35db623487f00d9392d8af44a24516d6cb9f274afaf73cfcfe180b9c54e007d2",
"sha256:40ec0a7f611a3d00d3c666c4cceb9aa3f5bf9fbd81392948a93663064f527203",
"sha256:47a03bfef80d6812c91ed6fae43f04f2fa80a4e1b82b35aa4d9002e39529e0b8",
"sha256:4b21d46728f8a6be537716035b445e7ef3a75dbd30bd31aa1b251323219d853e",
"sha256:4d1a806252001c5db7caecbe1a26e49a6c23421d85a700960f6ba093112f54a1",
"sha256:60e20a4ab4d4fec253557d0fc9a4e4095c37b664f78c72af24860c8adcd07088",
"sha256:9f61cca5262840ff46ef857d4f5f65679b82188709d0e5e086a9123791f721c8",
"sha256:a15835c8409d5edc50b4af93be3377b5dd3eb53517e7f785060df1f06f6da0e2",
"sha256:b39508562ad0bb3f384b0db24da7d68a2608b9ddc85b1d931ccaaa92d5e45273",
"sha256:ed60848caadeacecefd0b1de81b91beff23960032cded0ac1449242b506a3b3f",
"sha256:fc714895b6de6803ac9f661abb316853d0cd657f5d23985222255ad76ccedc25"
],
"index": "pypi",
"version": "==1.1.0"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"version": "==2.8.1"
},
"pytz": {
"hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
"sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
],
"version": "==2020.1"
},
"scikit-learn": {
"hashes": [
"sha256:04799686060ecbf8992f26a35be1d99e981894c8c7860c1365cda4200f954a16",
"sha256:058d213092de4384710137af1300ed0ff030b8c40459a6c6f73c31ccd274cc39",
"sha256:0c3464e46ef8bd4f1bfa5c009648c6449412c8f7e9b3fc0c9e3d800139c48827",
"sha256:0e7b55f73b35537ecd0d19df29dd39aa9e076dba78f3507b8136c819d84611fd",
"sha256:16feae4361be6b299d4d08df5a30956b4bfc8eadf173fe9258f6d59630f851d4",
"sha256:244ca85d6eba17a1e6e8a66ab2f584be6a7784b5f59297e3d7ff8c7983af627c",
"sha256:3e6e92b495eee193a8fa12a230c9b7976ea0fc1263719338e35c986ea1e42cff",
"sha256:5bcea4d6ee431c814261117281363208408aa4e665633655895feb059021aca6",
"sha256:93f56abd316d131645559ec0ab4f45e3391c2ccdd4eadaa4912f4c1e0a6f2c96",
"sha256:9e04c0811ea92931ee8490d638171b8cb2f21387efcfff526bbc8c2a3da60f1c",
"sha256:bded94236e16774385202cafd26190ce96db18e4dc21e99473848c61e4fdc400",
"sha256:c2fa33d20408b513cf432505c80e6eb4bf4d71434f1ae36680765d4a2c2a16ec",
"sha256:e3fec1c8831f8f93ad85581ca29ca1bb88e2da377fb097cf8322aa89c21bc9b8",
"sha256:e585682e37f2faa81ad6cd4472fff646bf2fd0542147bec93697a905db8e6bd2",
"sha256:e9879ba9e64ec3add41bf201e06034162f853652ef4849b361d73b0deb3153ad",
"sha256:ebe853e6f318f9d8b3b74dd17e553720d35646eff675a69eeaed12fbbbb07daa"
],
"version": "==0.23.1"
},
"scipy": {
"hashes": [
"sha256:066c513d90eb3fd7567a9e150828d39111ebd88d3e924cdfc9f8ce19ab6f90c9",
"sha256:07e52b316b40a4f001667d1ad4eb5f2318738de34597bd91537851365b6c61f1",
"sha256:0a0e9a4e58a4734c2eba917f834b25b7e3b6dc333901ce7784fd31aefbd37b2f",
"sha256:1c7564a4810c1cd77fcdee7fa726d7d39d4e2695ad252d7c86c3ea9d85b7fb8f",
"sha256:315aa2165aca31375f4e26c230188db192ed901761390be908c9b21d8b07df62",
"sha256:6e86c873fe1335d88b7a4bfa09d021f27a9e753758fd75f3f92d714aa4093768",
"sha256:8e28e74b97fc8d6aa0454989db3b5d36fc27e69cef39a7ee5eaf8174ca1123cb",
"sha256:92eb04041d371fea828858e4fff182453c25ae3eaa8782d9b6c32b25857d23bc",
"sha256:a0afbb967fd2c98efad5f4c24439a640d39463282040a88e8e928db647d8ac3d",
"sha256:a785409c0fa51764766840185a34f96a0a93527a0ff0230484d33a8ed085c8f8",
"sha256:cca9fce15109a36a0a9f9cfc64f870f1c140cb235ddf27fe0328e6afb44dfed0",
"sha256:d56b10d8ed72ec1be76bf10508446df60954f08a41c2d40778bc29a3a9ad9bce",
"sha256:dac09281a0eacd59974e24525a3bc90fa39b4e95177e638a31b14db60d3fa806",
"sha256:ec5fe57e46828d034775b00cd625c4a7b5c7d2e354c3b258d820c6c72212a6ec",
"sha256:eecf40fa87eeda53e8e11d265ff2254729d04000cd40bae648e76ff268885d66",
"sha256:fc98f3eac993b9bfdd392e675dfe19850cc8c7246a8fd2b42443e506344be7d9"
],
"version": "==1.5.2"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
},
"sklearn": {
"hashes": [
"sha256:e23001573aa194b834122d2b9562459bf5ae494a2d59ca6b8aa22c85a44c0e31"
],
"index": "pypi",
"version": "==0.0"
},
"sqlparse": {
"hashes": [
"sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
"sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
],
"version": "==0.3.1"
},
"threadpoolctl": {
"hashes": [
"sha256:38b74ca20ff3bb42caca8b00055111d74159ee95c4370882bbff2b93d24da725",
"sha256:ddc57c96a38beb63db45d6c159b5ab07b6bced12c45a1f07b2b92f272aebfa6b"
],
"version": "==2.1.0"
}
},
"develop": {}
}

25
README.md Normal file
View file

@ -0,0 +1,25 @@
# Exercice dev python
MeilleureCopro souhaite développer une API de statistiques sur des données dannonces immobilières pour des besoins internes.
Afin de valider cette demande, votre objectif est de construire en un minimum de temps une API qui répondra aux besoins suivants des utilisateurs internes MeilleureCopro :
- L'utilisateur doit pouvoir connaître les charges de copropriétés moyennes, les quantiles 10%, 90% sur un département, une ville ou code postal
- L'utilisateur interne doit pouvoir très simplement interroger la base de données via un navigateur
- L'utilisateur doit pouvoir rentrer les informations suivantes sur un appartement au sein d'une copropriété :
- une adresse,
- la surface de l'appartement,
- la présence d'ascenseur,
- la présence de chauffage collectif
et obtenir une estimation des charges de l'appartement. Vous utiliserez un modèle pour faire cette estimation.
Voici le dataset dannonces immobilières sur lequel vous calculerez les statistiques demandées : [dataset](https://storage.googleapis.com/data.meilleurecopro.com/stage/dataset_annonces.csv.tar.gz).
## Outils choisis
- pandas
- scikit-learn
- Django
- Django REST Framework

0
api/__init__.py Normal file
View file

3
api/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
api/apps.py Normal file
View file

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

1568
api/data/model.ipynb Normal file

File diff suppressed because it is too large Load diff

83
api/data/prediction.py Normal file
View file

@ -0,0 +1,83 @@
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
# TODO: use one hot encoding to handle unknown values in transform step
from sklearn.preprocessing import LabelEncoder
from .statistics import data_path
class LinearRegressor:
def __init__(self):
self.X, self.y = self.preprocessing()
def preprocessing(self):
"""Prepare data for modelling."""
# read csv
raw_data = pd.read_csv(
data_path, dtype={'DEPT_CODE': str, 'ZIP_CODE': str, 'INSEE_CODE': str})
# drop missing values from significative columns
feature_cols = ['CONDOMINIUM_EXPENSES',
'DEPT_CODE', 'ZIP_CODE', 'CITY', 'SURFACE']
data = raw_data.dropna(subset=feature_cols)
# columns used in modelling
cols = ['ZIP_CODE', 'DEPT_CODE', 'CITY', 'INSEE_CODE', 'LATITUDE',
'LONGITUDE', 'SURFACE', 'HEATING_MODE', 'ELEVATOR']
# target
y = data['CONDOMINIUM_EXPENSES']
# features
X = data[cols]
# surface rows are not correctly typed I do it manually
X['SURFACE'] = X['SURFACE'].astype(float)
# split dataset for training
X_train, X_val, y_train, y_val = train_test_split(
X, y, train_size=0.7, random_state=0)
# fill missing values with most frequent values. TODO: suboptimal method
train_mode = dict(X_train.mode().iloc[0])
X_train = X_train.fillna(train_mode)
cols = X_train.dtypes == 'object'
cat_cols = list(cols[cols].index)
cols = X_train.dtypes != 'object'
num_cols = list(cols[cols].index)
# handle missing values on categorical columns
imputer = SimpleImputer(strategy='constant')
X_train[cat_cols] = imputer.fit_transform(X_train[cat_cols])
X_val[cat_cols] = imputer.transform(X_val[cat_cols])
# encode categorical data
encoder = LabelEncoder()
for col in cat_cols:
X_train[col] = encoder.fit_transform(X_train[col])
# handle numerical missing values
num_imputer = SimpleImputer(strategy='mean')
X_train[num_cols] = num_imputer.fit_transform(X_train[num_cols])
X_val[num_cols] = num_imputer.transform(X_val[num_cols])
return X_train, y_train
def get_trained_model(self):
lin_reg = LinearRegression().fit(self.X, self.y)
return lin_reg
def predict_expenses(self, data):
"""Return condominium expenses as a function of data."""
model = self.get_trained_model()
return model.predict(data)

65
api/data/statistics.py Normal file
View file

@ -0,0 +1,65 @@
import os
import pandas as pd
from copro.settings import BASE_DIR
# TODO: put in env var and import it
data_path = os.path.join(BASE_DIR, 'api/data/annonces.csv')
def get_data_from_csv(path):
"""
Create dataframe from csv filepath. Keep only needed columns
`path` is the csv file location.
"""
# import data as dataframe
raw_data = pd.read_csv(path)
# keep only useful columns
feature_cols = ['CONDOMINIUM_EXPENSES', 'DEPT_CODE', 'ZIP_CODE', 'CITY']
data = raw_data.dropna(subset=feature_cols)
return data
def get_condo_expenses_by(col, value):
"""
Return mean, 1st and 9th decile condominium expenses values for the given query type.
`Col` can either be: `DEPT_CODE`, `CITY` or `ZIP_CODE`.
`Value` is the actual query parameter.
"""
assert col in ('DEPT_CODE', 'CITY',
'ZIP_CODE'), "col must be 'dept', 'city' or 'zip'"
data = get_data_from_csv(data_path)
# group data by column and compute statistics
group = data.groupby(col, as_index=False)['CONDOMINIUM_EXPENSES']
mean = group.mean()
first_quantile = group.quantile(0.1)
ninth_quantile = group.quantile(0.9)
# build filtering condition
if col == 'DEPT_CODE':
condition = mean['DEPT_CODE'] == value
elif col == 'CITY':
condition = mean['CITY'] == value
else:
condition = mean['ZIP_CODE'] == value
# TODO: refactor
mean = mean['CONDOMINIUM_EXPENSES'][condition].iloc[0]
first_quantile = first_quantile['CONDOMINIUM_EXPENSES'][condition].iloc[0]
ninth_quantile = ninth_quantile['CONDOMINIUM_EXPENSES'][condition].iloc[0]
return {
"mean": mean,
"1st_quantile": first_quantile,
"9th_quantile": ninth_quantile,
}

18
api/models.py Normal file
View file

@ -0,0 +1,18 @@
from django.db import models
class CondominiumExpense(models.Model):
mean = models.FloatField()
first_quantile = models.FloatField()
ninth_quantile = models.FloatField()
class CondominiumExpenseQuery(models.Model):
QUERY_TYPES = [
('DEPT_CODE', 'Department'),
('ZIP_CODE', 'Postal Code'),
('CITY', 'City'),
]
query_type = models.CharField(
max_length=20, choices=QUERY_TYPES, default='DEPT_CODE')
value = models.CharField(max_length=40, blank=True, default="")

15
api/serializers.py Normal file
View file

@ -0,0 +1,15 @@
from rest_framework import serializers
from .models import CondominiumExpense, CondominiumExpenseQuery
class CondominiumSerializer(serializers.ModelSerializer):
class Meta:
model = CondominiumExpense
fields = '__all__'
class CondominiumQuerySerializer(serializers.ModelSerializer):
class Meta:
model = CondominiumExpenseQuery
fields = '__all__'

3
api/tests.py Normal file
View file

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

8
api/urls.py Normal file
View file

@ -0,0 +1,8 @@
from django.urls import path
from .views import get_condominium_expenses, make_expenses_prediction
urlpatterns = [
path('api/v1/', get_condominium_expenses),
path('api/v1/predict/', make_expenses_prediction),
]

29
api/views.py Normal file
View file

@ -0,0 +1,29 @@
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .data.prediction import LinearRegressor
from .data.statistics import get_condo_expenses_by
@api_view(['GET', 'POST'])
def get_condominium_expenses(request):
"""TODO: refactor using APIView class"""
data = {}
if request.method == "POST":
query_type = request.data["query_type"]
query_val = request.data['query_val']
data = get_condo_expenses_by(query_type, query_val)
return Response(data)
@api_view(['POST'])
def make_expenses_prediction(request):
"""Return condominium expenses prediction."""
algo = LinearRegressor
prediction = algo.predict_expenses(request.data)
return Response(prediction)

0
copro/__init__.py Normal file
View file

16
copro/asgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
ASGI config for copro 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/3.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'copro.settings')
application = get_asgi_application()

122
copro/settings.py Normal file
View file

@ -0,0 +1,122 @@
"""
Django settings for copro project.
Generated by 'django-admin startproject' using Django 3.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/3.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '6)akoq-jb7%!ydxtji%ss+xk7o&@e-&=eep(4r2%3zz^^&4wzv'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'api.apps.ApiConfig'
]
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 = 'copro.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'copro.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/3.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/3.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'

22
copro/urls.py Normal file
View file

@ -0,0 +1,22 @@
"""copro URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.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 include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('api.urls'))
]

16
copro/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for copro 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/3.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'copro.settings')
application = get_wsgi_application()

21
manage.py Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'copro.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()