From 5c51e0f1e6a6399ab986fd29985cce08b7dbee30 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Thu, 6 Oct 2022 16:24:55 +0330 Subject: [PATCH 01/19] Initial project --- .gitignore | 281 ++++++++++++++++++++++++++++++++++++++ crypto_reader/__init__.py | 0 crypto_reader/asgi.py | 16 +++ crypto_reader/settings.py | 123 +++++++++++++++++ crypto_reader/urls.py | 21 +++ crypto_reader/wsgi.py | 16 +++ manage.py | 22 +++ requirements.txt | 16 +++ 8 files changed, 495 insertions(+) create mode 100644 .gitignore create mode 100644 crypto_reader/__init__.py create mode 100644 crypto_reader/asgi.py create mode 100644 crypto_reader/settings.py create mode 100644 crypto_reader/urls.py create mode 100644 crypto_reader/wsgi.py create mode 100755 manage.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1cfbf63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,281 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,django +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,django + +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ +# in your Git repository. Update and uncomment the following line accordingly. +# /staticfiles/ + +### Django.Python Stack ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo + +# Django stuff: + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python ### +# Byte-compiled / optimized / DLL files + +# C extensions + +# Distribution / packaging + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. + +# Installer logs + +# Unit test / coverage reports + +# Translations + +# Django stuff: + +# Flask stuff: + +# Scrapy stuff: + +# Sphinx documentation + +# PyBuilder + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm + +# Celery stuff + +# SageMath parsed files + +# Environments + +# Spyder project settings + +# Rope project settings + +# mkdocs documentation + +# mypy + +# Pyre type checker + +# pytype static type analyzer + +# Cython debug symbols + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. + +### VisualStudioCode ### +.vscode/* +.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,django diff --git a/crypto_reader/__init__.py b/crypto_reader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crypto_reader/asgi.py b/crypto_reader/asgi.py new file mode 100644 index 0000000..f13fb13 --- /dev/null +++ b/crypto_reader/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for crypto_reader 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/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crypto_reader.settings') + +application = get_asgi_application() diff --git a/crypto_reader/settings.py b/crypto_reader/settings.py new file mode 100644 index 0000000..699ff03 --- /dev/null +++ b/crypto_reader/settings.py @@ -0,0 +1,123 @@ +""" +Django settings for crypto_reader project. + +Generated by 'django-admin startproject' using Django 4.1.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-r1j8m$^@^n9iyf9rfouhh0cv6pf^6$p=d-6ng1039elb6^a_p=' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'crypto_reader.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 = 'crypto_reader.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/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/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/crypto_reader/urls.py b/crypto_reader/urls.py new file mode 100644 index 0000000..61a2e82 --- /dev/null +++ b/crypto_reader/urls.py @@ -0,0 +1,21 @@ +"""crypto_reader URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.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.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/crypto_reader/wsgi.py b/crypto_reader/wsgi.py new file mode 100644 index 0000000..eab201b --- /dev/null +++ b/crypto_reader/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for crypto_reader 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/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crypto_reader.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..9d66408 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crypto_reader.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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e9b12a8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +asgiref==3.5.2 +astroid==2.12.10 +autopep8==1.7.0 +dill==0.3.5.1 +Django==4.1.2 +isort==5.10.1 +lazy-object-proxy==1.7.1 +mccabe==0.7.0 +platformdirs==2.5.2 +pycodestyle==2.9.1 +pylint==2.15.3 +sqlparse==0.4.3 +toml==0.10.2 +tomli==2.0.1 +tomlkit==0.11.5 +wrapt==1.14.1 From 63f03a0615a31ad23cdc792c1ba6a2a9274346f4 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Thu, 6 Oct 2022 16:35:11 +0330 Subject: [PATCH 02/19] Initial accounts app --- accounts/__init__.py | 0 accounts/admin.py | 3 +++ accounts/apps.py | 6 ++++++ accounts/migrations/__init__.py | 0 accounts/models.py | 3 +++ accounts/tests.py | 3 +++ accounts/views.py | 3 +++ crypto_reader/settings.py | 2 ++ 8 files changed, 20 insertions(+) create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/tests.py create mode 100644 accounts/views.py diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/crypto_reader/settings.py b/crypto_reader/settings.py index 699ff03..a425d09 100644 --- a/crypto_reader/settings.py +++ b/crypto_reader/settings.py @@ -37,6 +37,8 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + + 'accounts', ] MIDDLEWARE = [ From 9b2bb8806fa75fd43339b001f7053f08b68330ce Mon Sep 17 00:00:00 2001 From: HamidReza Date: Thu, 6 Oct 2022 19:01:19 +0330 Subject: [PATCH 03/19] Create User model + Add Login & Register --- accounts/admin.py | 18 +++++++- accounts/migrations/0001_initial.py | 44 +++++++++++++++++++ accounts/models.py | 66 ++++++++++++++++++++++++++++- accounts/serializers.py | 35 +++++++++++++++ accounts/urls.py | 16 +++++++ accounts/views.py | 21 ++++++++- crypto_reader/settings.py | 18 ++++++++ crypto_reader/urls.py | 3 +- requirements.txt | 4 ++ 9 files changed, 220 insertions(+), 5 deletions(-) create mode 100644 accounts/migrations/0001_initial.py create mode 100644 accounts/serializers.py create mode 100644 accounts/urls.py diff --git a/accounts/admin.py b/accounts/admin.py index 8c38f3f..0d31c30 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,3 +1,19 @@ from django.contrib import admin +from .models import User +from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin -# Register your models here. + +@admin.register(User) +class UserAdmin(DefaultUserAdmin): + fieldsets = ( + (None, {'fields': ('username', 'password')}), + ('Personal info', + { + 'fields': + ('name', 'kucoin_key', 'kucoin_secret', 'kucoin_passphrase')}), + ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), }), + ('Important dates', {'fields': ('last_login', 'date_joined')}),) + + list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') + list_display = ('username', 'name', 'is_staff', 'is_active') + list_editable = ('is_staff', 'is_active') diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..9e2f495 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.1.2 on 2022-10-06 14:59 + +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('name', models.CharField(max_length=150, verbose_name='name')), + ('kucoin_key', models.CharField(max_length=200)), + ('kucoin_secret', models.CharField(max_length=200)), + ('kucoin_passphrase', models.CharField(max_length=200)), + ('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={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 71a8362..f2a1267 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1,67 @@ from django.db import models +from django.contrib.auth.models import AbstractUser +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.base_user import BaseUserManager +from django.utils import timezone +from django.core.signing import Signer -# Create your models here. +signer = Signer() + + +class UserManager(BaseUserManager): + ''' + UserManager to create user and encrypts kucoin details and save to the database. + ''' + + def encryption(self, key, secret, passphrase) -> tuple: + return signer.sign(key), signer.sign(secret), signer.sign(passphrase) + + def create_user(self, name, username, kucoin_key, kucoin_secret, kucoin_passphrase, password=None): + + now = timezone.now() + + kucoin_key, kucoin_secret, kucoin_passphrase = self.encryption(kucoin_key, kucoin_secret, kucoin_passphrase) + + user = self.model( + name=name, username=username, kucoin_key=kucoin_key, kucoin_secret=kucoin_secret, + kucoin_passphrase=kucoin_passphrase, is_staff=False, is_active=True, is_superuser=False, date_joined=now, + last_login=now,) + user.set_password(password) + user.save(using=self._db) + + return user + + def create_superuser(self, name, username, password=None): + user = self.model(name=name, username=username) + user.is_staff = True + user.is_active = True + user.is_superuser = True + user.set_password(password) + user.save(using=self._db) + + return user + + +class User(AbstractUser): + ''' + User model with name, username & kucoin details + ''' + + name = models.CharField(_("name"), max_length=150) + kucoin_key = models.CharField(max_length=200) + kucoin_secret = models.CharField(max_length=200) + kucoin_passphrase = models.CharField(max_length=200) + + objects = UserManager() + + REQUIRED_FIELDS = ['name'] + + def __str__(self) -> str: + return self.username + + def decryption(self, key, secret, passphrase) -> tuple: + ''' + Decrypt kucoin details of the user + ''' + + return signer.unsign(key), signer.unsign(secret), signer.unsign(passphrase) diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..6118f30 --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,35 @@ +from rest_framework import serializers +from .models import User + + +class RegisterSerializer(serializers.ModelSerializer): + ''' + User serializers for register + ''' + + confirm_password = serializers.CharField(write_only=True, required=True) + + class Meta: + model = User + fields = ("username", "name", "password", "confirm_password", + "kucoin_key", "kucoin_secret", "kucoin_passphrase") + extra_kwargs = { + 'kucoin_key': {'write_only': True, 'required': True}, + 'kucoin_secret': {'write_only': True, 'required': True}, + 'kucoin_passphrase': {'write_only': True, 'required': True}, + } + + def validate(self, attrs) -> dict: + ''' + Checking password & other fields + ''' + + if attrs['password'] != attrs['confirm_password']: + raise serializers.ValidationError({"password": "Password fields didn't match."}) + + return super().validate(attrs) + + def create(self, validated_data) -> User: + validated_data.pop('confirm_password') + + return User.objects.create_user(**validated_data) diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..abcc6b3 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from .views import Register +from rest_framework_simplejwt.views import ( + TokenRefreshView, + TokenObtainPairView, +) + +app_name = "accounts" + +urlpatterns = [ + path('register/', Register.as_view(), name="register"), # register user + + # JWTAuthentication + path('login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # login user with jwt + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), # refresh token in jwt +] diff --git a/accounts/views.py b/accounts/views.py index 91ea44a..59c3c57 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,3 +1,20 @@ -from django.shortcuts import render +from rest_framework.generics import CreateAPIView +from .serializers import RegisterSerializer +from rest_framework.permissions import AllowAny +from rest_framework.response import Response -# Create your views here. + +class Register(CreateAPIView): + ''' + Signup users with given fields. + ''' + + serializer_class = RegisterSerializer + permission_classes = (AllowAny,) + + def create(self, request) -> Response: + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + + return Response({"message": "Account created successfully."}, status=201) diff --git a/crypto_reader/settings.py b/crypto_reader/settings.py index a425d09..d0181df 100644 --- a/crypto_reader/settings.py +++ b/crypto_reader/settings.py @@ -11,6 +11,7 @@ """ from pathlib import Path +from datetime import timedelta # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -27,6 +28,7 @@ ALLOWED_HOSTS = [] +AUTH_USER_MODEL = "accounts.User" # Application definition @@ -38,9 +40,25 @@ 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'rest_framework_simplejwt', + 'rest_framework_simplejwt.token_blacklist', 'accounts', ] +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], +} + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': True, + 'BLACKLIST_AFTER_ROTATION': True, +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/crypto_reader/urls.py b/crypto_reader/urls.py index 61a2e82..bf8ff88 100644 --- a/crypto_reader/urls.py +++ b/crypto_reader/urls.py @@ -14,8 +14,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path('accounts/', include('accounts.urls')), ] diff --git a/requirements.txt b/requirements.txt index e9b12a8..43c2362 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,12 +3,16 @@ astroid==2.12.10 autopep8==1.7.0 dill==0.3.5.1 Django==4.1.2 +djangorestframework==3.14.0 +djangorestframework-simplejwt==5.2.1 isort==5.10.1 lazy-object-proxy==1.7.1 mccabe==0.7.0 platformdirs==2.5.2 pycodestyle==2.9.1 +PyJWT==2.5.0 pylint==2.15.3 +pytz==2022.4 sqlparse==0.4.3 toml==0.10.2 tomli==2.0.1 From cd963609d52e27321929afeb46560d39081509dc Mon Sep 17 00:00:00 2001 From: HamidReza Date: Thu, 6 Oct 2022 19:29:29 +0330 Subject: [PATCH 04/19] Fix encryption --- accounts/encryption.py | 24 ++++++++++++++++++++++++ accounts/models.py | 5 +++-- crypto_reader/settings.py | 3 +++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 accounts/encryption.py diff --git a/accounts/encryption.py b/accounts/encryption.py new file mode 100644 index 0000000..522c32e --- /dev/null +++ b/accounts/encryption.py @@ -0,0 +1,24 @@ +from cryptography.fernet import Fernet +import base64 +from django.conf import settings + + +def encrypt(txt): + try: + txt = str(txt) + cipher_suite = Fernet(settings.ENCRYPT_KEY) + encrypted_text = cipher_suite.encrypt(txt.encode('ascii')) + encrypted_text = base64.urlsafe_b64encode(encrypted_text).decode("ascii") + return encrypted_text + except Exception: + return None + + +def decrypt(txt): + try: + txt = base64.urlsafe_b64decode(txt) + cipher_suite = Fernet(settings.ENCRYPT_KEY) + decoded_text = cipher_suite.decrypt(txt).decode("ascii") + return decoded_text + except Exception: + return None diff --git a/accounts/models.py b/accounts/models.py index f2a1267..e9e482a 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -4,6 +4,7 @@ from django.contrib.auth.base_user import BaseUserManager from django.utils import timezone from django.core.signing import Signer +from .encryption import encrypt, decrypt signer = Signer() @@ -14,7 +15,7 @@ class UserManager(BaseUserManager): ''' def encryption(self, key, secret, passphrase) -> tuple: - return signer.sign(key), signer.sign(secret), signer.sign(passphrase) + return encrypt(key), encrypt(secret), encrypt(passphrase) def create_user(self, name, username, kucoin_key, kucoin_secret, kucoin_passphrase, password=None): @@ -64,4 +65,4 @@ def decryption(self, key, secret, passphrase) -> tuple: Decrypt kucoin details of the user ''' - return signer.unsign(key), signer.unsign(secret), signer.unsign(passphrase) + return decrypt(key), decrypt(secret), decrypt(passphrase) diff --git a/crypto_reader/settings.py b/crypto_reader/settings.py index d0181df..d33009a 100644 --- a/crypto_reader/settings.py +++ b/crypto_reader/settings.py @@ -26,6 +26,9 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True +# SECURITY WARNING: keep the encrypt key used in production secret! +ENCRYPT_KEY = b'7lXZs9UHMEQzmjRABWyTcEV6aUuy8BvxWj5Ee7yGOgI=' + ALLOWED_HOSTS = [] AUTH_USER_MODEL = "accounts.User" From 22cbde9d6209fb0bb0f278fc8b2b8a97a1c1fadd Mon Sep 17 00:00:00 2001 From: HamidReza Date: Thu, 6 Oct 2022 20:37:54 +0330 Subject: [PATCH 05/19] Initial kucoin app --- crypto_reader/settings.py | 1 + crypto_reader/urls.py | 1 + kucoin/__init__.py | 0 kucoin/admin.py | 3 +++ kucoin/apps.py | 6 ++++++ kucoin/migrations/__init__.py | 0 kucoin/models.py | 3 +++ kucoin/tests.py | 3 +++ kucoin/urls.py | 7 +++++++ kucoin/views.py | 3 +++ 10 files changed, 27 insertions(+) create mode 100644 kucoin/__init__.py create mode 100644 kucoin/admin.py create mode 100644 kucoin/apps.py create mode 100644 kucoin/migrations/__init__.py create mode 100644 kucoin/models.py create mode 100644 kucoin/tests.py create mode 100644 kucoin/urls.py create mode 100644 kucoin/views.py diff --git a/crypto_reader/settings.py b/crypto_reader/settings.py index d33009a..a8f43c7 100644 --- a/crypto_reader/settings.py +++ b/crypto_reader/settings.py @@ -47,6 +47,7 @@ 'rest_framework_simplejwt', 'rest_framework_simplejwt.token_blacklist', 'accounts', + 'kucoin', ] REST_FRAMEWORK = { diff --git a/crypto_reader/urls.py b/crypto_reader/urls.py index bf8ff88..7d88988 100644 --- a/crypto_reader/urls.py +++ b/crypto_reader/urls.py @@ -19,4 +19,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('accounts/', include('accounts.urls')), + path('kucoin/', include('kucoin.urls')), ] diff --git a/kucoin/__init__.py b/kucoin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kucoin/admin.py b/kucoin/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/kucoin/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/kucoin/apps.py b/kucoin/apps.py new file mode 100644 index 0000000..97a5812 --- /dev/null +++ b/kucoin/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class KucoinConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'kucoin' diff --git a/kucoin/migrations/__init__.py b/kucoin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kucoin/models.py b/kucoin/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/kucoin/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/kucoin/tests.py b/kucoin/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/kucoin/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/kucoin/urls.py b/kucoin/urls.py new file mode 100644 index 0000000..abf40c4 --- /dev/null +++ b/kucoin/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +app_name = "kucoin" + +urlpatterns = [ + +] diff --git a/kucoin/views.py b/kucoin/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/kucoin/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 1d70e2a5519912d3174b9c014f60bd9181191088 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 11:29:18 +0330 Subject: [PATCH 06/19] See list of current open positions --- kucoin/urls.py | 4 +++- kucoin/utils.py | 39 +++++++++++++++++++++++++++++++++++++++ kucoin/views.py | 20 ++++++++++++++++++-- requirements.txt | 8 ++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 kucoin/utils.py diff --git a/kucoin/urls.py b/kucoin/urls.py index abf40c4..bdd4d0b 100644 --- a/kucoin/urls.py +++ b/kucoin/urls.py @@ -1,7 +1,9 @@ from django.urls import path +from .views import OpenPositions + app_name = "kucoin" urlpatterns = [ - + path('open_positions/', OpenPositions.as_view(), name='open_positions'), # see list of current open positions ] diff --git a/kucoin/utils.py b/kucoin/utils.py new file mode 100644 index 0000000..c292b55 --- /dev/null +++ b/kucoin/utils.py @@ -0,0 +1,39 @@ +import time +import base64 +import hashlib +import hmac +import time +import requests +from accounts.encryption import decrypt + + +def kucoin_api(key, secret, passphrase, endpoint, params): + ''' + Get data from kucoin with specefic header + ''' + + url = 'https://api.kucoin.com' + endpoint + + key = decrypt(key) + secret = decrypt(secret) + passphrase = decrypt(passphrase) + + now = int(time.time() * 1000) + str_to_sign = str(now) + 'GET' + endpoint + '?' + params + signature = base64.b64encode( + hmac.new(secret.encode('utf-8'), str_to_sign.encode('utf-8'), hashlib.sha256).digest()) + passphrase = base64.b64encode( + hmac.new(secret.encode('utf-8'), + passphrase.encode('utf-8'), + hashlib.sha256).digest()) + + headers = { + "KC-API-SIGN": signature, + "KC-API-TIMESTAMP": str(now), + "KC-API-KEY": key, + "KC-API-PASSPHRASE": passphrase, + "KC-API-KEY-VERSION": "2" + } + + response = requests.request('get', url, headers=headers, params=params) + return response.status_code, response.json() diff --git a/kucoin/views.py b/kucoin/views.py index 91ea44a..d9e77c8 100644 --- a/kucoin/views.py +++ b/kucoin/views.py @@ -1,3 +1,19 @@ -from django.shortcuts import render +from rest_framework.generics import GenericAPIView +from .utils import kucoin_api +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated -# Create your views here. + +class OpenPositions(GenericAPIView): + ''' + Get and view open (active) positions. + ''' + + permission_classes = (IsAuthenticated,) + + def get(self, request): + user = request.user + status, response = kucoin_api(user.kucoin_key, user.kucoin_secret, + user.kucoin_passphrase, '/api/v1/orders', 'status=active') + + return Response(response, status=status) diff --git a/requirements.txt b/requirements.txt index 43c2362..ee54ab0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,28 @@ asgiref==3.5.2 astroid==2.12.10 autopep8==1.7.0 +certifi==2022.9.24 +cffi==1.15.1 +charset-normalizer==2.1.1 +cryptography==38.0.1 dill==0.3.5.1 Django==4.1.2 djangorestframework==3.14.0 djangorestframework-simplejwt==5.2.1 +idna==3.4 isort==5.10.1 lazy-object-proxy==1.7.1 mccabe==0.7.0 platformdirs==2.5.2 pycodestyle==2.9.1 +pycparser==2.21 PyJWT==2.5.0 pylint==2.15.3 pytz==2022.4 +requests==2.28.1 sqlparse==0.4.3 toml==0.10.2 tomli==2.0.1 tomlkit==0.11.5 +urllib3==1.26.12 wrapt==1.14.1 From 1689392e95ed2ffb2fbedaa096f9520c40a12b96 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 12:45:45 +0330 Subject: [PATCH 07/19] Add comments --- accounts/encryption.py | 12 ++++++++++-- accounts/models.py | 4 ++++ kucoin/utils.py | 4 +++- kucoin/views.py | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/accounts/encryption.py b/accounts/encryption.py index 522c32e..1e5e08c 100644 --- a/accounts/encryption.py +++ b/accounts/encryption.py @@ -3,7 +3,11 @@ from django.conf import settings -def encrypt(txt): +def encrypt(txt) -> str: + ''' + Encrypt data + ''' + try: txt = str(txt) cipher_suite = Fernet(settings.ENCRYPT_KEY) @@ -14,7 +18,11 @@ def encrypt(txt): return None -def decrypt(txt): +def decrypt(txt) -> str: + ''' + Decrypt data + ''' + try: txt = base64.urlsafe_b64decode(txt) cipher_suite = Fernet(settings.ENCRYPT_KEY) diff --git a/accounts/models.py b/accounts/models.py index e9e482a..8e5fab8 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -15,6 +15,10 @@ class UserManager(BaseUserManager): ''' def encryption(self, key, secret, passphrase) -> tuple: + ''' + Encrypt key, secret & passphrase + ''' + return encrypt(key), encrypt(secret), encrypt(passphrase) def create_user(self, name, username, kucoin_key, kucoin_secret, kucoin_passphrase, password=None): diff --git a/kucoin/utils.py b/kucoin/utils.py index c292b55..6a853eb 100644 --- a/kucoin/utils.py +++ b/kucoin/utils.py @@ -7,17 +7,19 @@ from accounts.encryption import decrypt -def kucoin_api(key, secret, passphrase, endpoint, params): +def kucoin_api(key, secret, passphrase, endpoint, params) -> tuple: ''' Get data from kucoin with specefic header ''' url = 'https://api.kucoin.com' + endpoint + # decrypt data key = decrypt(key) secret = decrypt(secret) passphrase = decrypt(passphrase) + # create signature now = int(time.time() * 1000) str_to_sign = str(now) + 'GET' + endpoint + '?' + params signature = base64.b64encode( diff --git a/kucoin/views.py b/kucoin/views.py index d9e77c8..aaf4d5b 100644 --- a/kucoin/views.py +++ b/kucoin/views.py @@ -11,7 +11,7 @@ class OpenPositions(GenericAPIView): permission_classes = (IsAuthenticated,) - def get(self, request): + def get(self, request) -> Response: user = request.user status, response = kucoin_api(user.kucoin_key, user.kucoin_secret, user.kucoin_passphrase, '/api/v1/orders', 'status=active') From 390dc4afa0b275f7bdfeca18549f3786a4a5eb3c Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 16:17:09 +0330 Subject: [PATCH 08/19] Add order model + celery task --- crypto_reader/__init__.py | 5 +++++ crypto_reader/celery.py | 22 +++++++++++++++++++++ crypto_reader/settings.py | 5 +++++ kucoin/admin.py | 8 +++++++- kucoin/migrations/0001_initial.py | 32 +++++++++++++++++++++++++++++++ kucoin/models.py | 13 ++++++++++++- kucoin/serializers.py | 20 +++++++++++++++++++ kucoin/tasks.py | 28 +++++++++++++++++++++++++++ kucoin/urls.py | 4 ++-- kucoin/utils.py | 23 +++++++++++++++++++--- kucoin/views.py | 29 ++++++++++++++++++++++++---- requirements.txt | 24 ++++++++++++++++++++++- 12 files changed, 201 insertions(+), 12 deletions(-) create mode 100644 crypto_reader/celery.py create mode 100644 kucoin/migrations/0001_initial.py create mode 100644 kucoin/serializers.py create mode 100644 kucoin/tasks.py diff --git a/crypto_reader/__init__.py b/crypto_reader/__init__.py index e69de29..15d7c50 100644 --- a/crypto_reader/__init__.py +++ b/crypto_reader/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/crypto_reader/celery.py b/crypto_reader/celery.py new file mode 100644 index 0000000..24f8470 --- /dev/null +++ b/crypto_reader/celery.py @@ -0,0 +1,22 @@ +import os + +from celery import Celery + +# Set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crypto_reader.settings') + +app = Celery('crypto_reader') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django apps. +app.autodiscover_tasks() + + +@app.task(bind=True, ignore_result=True) +def debug_task(self): + print(f'Request: {self.request!r}') diff --git a/crypto_reader/settings.py b/crypto_reader/settings.py index a8f43c7..bb813de 100644 --- a/crypto_reader/settings.py +++ b/crypto_reader/settings.py @@ -46,6 +46,7 @@ 'rest_framework', 'rest_framework_simplejwt', 'rest_framework_simplejwt.token_blacklist', + 'django_celery_beat', 'accounts', 'kucoin', ] @@ -145,3 +146,7 @@ # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# celery settings +CELERY_BROKER_URL = 'redis://localhost:6379/0' +CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' diff --git a/kucoin/admin.py b/kucoin/admin.py index 8c38f3f..3204b1c 100644 --- a/kucoin/admin.py +++ b/kucoin/admin.py @@ -1,3 +1,9 @@ from django.contrib import admin +from .models import Order -# Register your models here. + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_filter = ('isActive',) + list_display = ('user', 'clientOid', 'isActive') + list_editable = ('isActive',) diff --git a/kucoin/migrations/0001_initial.py b/kucoin/migrations/0001_initial.py new file mode 100644 index 0000000..262efb0 --- /dev/null +++ b/kucoin/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.8 on 2022-10-07 11:46 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('clientOid', models.CharField(max_length=200)), + ('side', models.CharField(max_length=200)), + ('symbol', models.CharField(max_length=200)), + ('type', models.CharField(max_length=200)), + ('remark', models.CharField(max_length=200)), + ('stp', models.CharField(max_length=200)), + ('tradeType', models.CharField(max_length=200)), + ('isActive', models.BooleanField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/kucoin/models.py b/kucoin/models.py index 71a8362..da784b5 100644 --- a/kucoin/models.py +++ b/kucoin/models.py @@ -1,3 +1,14 @@ from django.db import models +from accounts.models import User -# Create your models here. + +class Order(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + clientOid = models.CharField(max_length=200) + side = models.CharField(max_length=200) + symbol = models.CharField(max_length=200) + type = models.CharField(max_length=200) + remark = models.CharField(max_length=200) + stp = models.CharField(max_length=200) + tradeType = models.CharField(max_length=200) + isActive = models.BooleanField() diff --git a/kucoin/serializers.py b/kucoin/serializers.py new file mode 100644 index 0000000..03463cb --- /dev/null +++ b/kucoin/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers +from kucoin.models import Order + + +class OrderSerializers(serializers.ModelSerializer): + ''' + Serializer for order model with all fields + ''' + + class Meta: + model = Order + fields = '__all__' + + +class TrackSerializers(serializers.Serializer): + ''' + track boolean field + ''' + + track = serializers.BooleanField() diff --git a/kucoin/tasks.py b/kucoin/tasks.py new file mode 100644 index 0000000..e0e8de1 --- /dev/null +++ b/kucoin/tasks.py @@ -0,0 +1,28 @@ +from .utils import kucoin_api +from accounts.models import User +from .models import Order +from crypto_reader.celery import app + + +@app.task(name='kucoin.tasks.tracking_position_per_user') +def tracking(user_pk): + ''' + Tracking position of each user every 30 seconds + ''' + + user = User.objects.get(pk=user_pk) + status, response = kucoin_api(user.kucoin_key, user.kucoin_secret, + user.kucoin_passphrase, '/api/v1/orders') + + if response['code'] == '200000': + items = response['items'] + + for item in items: + Order.objects.update_or_create( + user=user, clientOid=item['clientOid'], + side=item['side'], + symbol=item['symbol'], + type=item['type'], + remark=item['remark'], + stp=item['stp'], + tradeType=item['tradeType'], isActive=item['isActive']) diff --git a/kucoin/urls.py b/kucoin/urls.py index bdd4d0b..0da0287 100644 --- a/kucoin/urls.py +++ b/kucoin/urls.py @@ -1,9 +1,9 @@ from django.urls import path - -from .views import OpenPositions +from .views import OpenPositions, TrackPositions app_name = "kucoin" urlpatterns = [ path('open_positions/', OpenPositions.as_view(), name='open_positions'), # see list of current open positions + path('tracking/', TrackPositions.as_view(), name='tracking'), # enable or disable tracking position ] diff --git a/kucoin/utils.py b/kucoin/utils.py index 6a853eb..4ebffd6 100644 --- a/kucoin/utils.py +++ b/kucoin/utils.py @@ -3,11 +3,15 @@ import hashlib import hmac import time +import json import requests from accounts.encryption import decrypt +from django_celery_beat.models import PeriodicTask, IntervalSchedule +schedule, created = IntervalSchedule.objects.get_or_create(every=30, period=IntervalSchedule.SECONDS,) -def kucoin_api(key, secret, passphrase, endpoint, params) -> tuple: + +def kucoin_api(key, secret, passphrase, endpoint) -> tuple: ''' Get data from kucoin with specefic header ''' @@ -21,7 +25,7 @@ def kucoin_api(key, secret, passphrase, endpoint, params) -> tuple: # create signature now = int(time.time() * 1000) - str_to_sign = str(now) + 'GET' + endpoint + '?' + params + str_to_sign = str(now) + 'GET' + endpoint signature = base64.b64encode( hmac.new(secret.encode('utf-8'), str_to_sign.encode('utf-8'), hashlib.sha256).digest()) passphrase = base64.b64encode( @@ -37,5 +41,18 @@ def kucoin_api(key, secret, passphrase, endpoint, params) -> tuple: "KC-API-KEY-VERSION": "2" } - response = requests.request('get', url, headers=headers, params=params) + response = requests.request('get', url, headers=headers) return response.status_code, response.json() + + +def create_or_delete_celery_task(user, track): + if track: + PeriodicTask.objects.get_or_create(interval=schedule, name=f"User({user.pk})", + task='kucoin.tasks.tracking_position_per_user', + args=json.dumps([f"{user.pk}"]),) + + return {'message': 'Tracking Enabled'} + + PeriodicTask.objects.filter(name=f"User({user.pk})").delete() + + return {'message': 'Tracking Disabled'} diff --git a/kucoin/views.py b/kucoin/views.py index aaf4d5b..02da6a1 100644 --- a/kucoin/views.py +++ b/kucoin/views.py @@ -1,7 +1,9 @@ from rest_framework.generics import GenericAPIView -from .utils import kucoin_api +from kucoin.models import Order from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated +from .serializers import OrderSerializers, TrackSerializers +from .utils import create_or_delete_celery_task class OpenPositions(GenericAPIView): @@ -10,10 +12,29 @@ class OpenPositions(GenericAPIView): ''' permission_classes = (IsAuthenticated,) + serializer_class = OrderSerializers def get(self, request) -> Response: user = request.user - status, response = kucoin_api(user.kucoin_key, user.kucoin_secret, - user.kucoin_passphrase, '/api/v1/orders', 'status=active') + orders = Order.objects.filter(user=user, isActive=True) + data = OrderSerializers(orders, many=True).data - return Response(response, status=status) + return Response(data, status=200) + + +class TrackPositions(GenericAPIView): + ''' + Enable or disable Tracking positions for each user + ''' + + permission_classes = (IsAuthenticated,) + serializer_class = TrackSerializers + + def post(self, request): + user = request.user + serializer = TrackSerializers(data=request.data) + serializer.is_valid(raise_exception=True) + track = serializer.data['track'] + data = create_or_delete_celery_task(user, track) + + return Response(data, status=200) diff --git a/requirements.txt b/requirements.txt index ee54ab0..788485d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,28 +1,50 @@ +amqp==5.1.1 asgiref==3.5.2 astroid==2.12.10 +async-timeout==4.0.2 autopep8==1.7.0 +billiard==3.6.4.0 +celery==5.2.7 certifi==2022.9.24 cffi==1.15.1 charset-normalizer==2.1.1 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 cryptography==38.0.1 +Deprecated==1.2.13 dill==0.3.5.1 -Django==4.1.2 +Django==4.0.8 +django-celery-beat==2.3.0 +django-timezone-field==5.0 djangorestframework==3.14.0 djangorestframework-simplejwt==5.2.1 idna==3.4 isort==5.10.1 +kombu==5.2.4 lazy-object-proxy==1.7.1 mccabe==0.7.0 +packaging==21.3 platformdirs==2.5.2 +prompt-toolkit==3.0.31 pycodestyle==2.9.1 pycparser==2.21 PyJWT==2.5.0 pylint==2.15.3 +pyparsing==3.0.9 +python-crontab==2.6.0 +python-dateutil==2.8.2 pytz==2022.4 +redis==4.3.4 requests==2.28.1 +six==1.16.0 sqlparse==0.4.3 toml==0.10.2 tomli==2.0.1 tomlkit==0.11.5 +tzdata==2022.4 urllib3==1.26.12 +vine==5.0.0 +wcwidth==0.2.5 wrapt==1.14.1 From 9e4d9f2337c13c0f50c7b1f943f876d7bda508e9 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 17:21:40 +0330 Subject: [PATCH 09/19] Add UpdateOpenPositions --- kucoin/tasks.py | 19 ++----------------- kucoin/urls.py | 4 +++- kucoin/utils.py | 32 ++++++++++++++++++++++++++++++++ kucoin/views.py | 16 +++++++++++++++- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/kucoin/tasks.py b/kucoin/tasks.py index e0e8de1..fd37a49 100644 --- a/kucoin/tasks.py +++ b/kucoin/tasks.py @@ -1,6 +1,5 @@ -from .utils import kucoin_api +from .utils import update_orders from accounts.models import User -from .models import Order from crypto_reader.celery import app @@ -11,18 +10,4 @@ def tracking(user_pk): ''' user = User.objects.get(pk=user_pk) - status, response = kucoin_api(user.kucoin_key, user.kucoin_secret, - user.kucoin_passphrase, '/api/v1/orders') - - if response['code'] == '200000': - items = response['items'] - - for item in items: - Order.objects.update_or_create( - user=user, clientOid=item['clientOid'], - side=item['side'], - symbol=item['symbol'], - type=item['type'], - remark=item['remark'], - stp=item['stp'], - tradeType=item['tradeType'], isActive=item['isActive']) + update_orders(user) diff --git a/kucoin/urls.py b/kucoin/urls.py index 0da0287..048c414 100644 --- a/kucoin/urls.py +++ b/kucoin/urls.py @@ -1,9 +1,11 @@ from django.urls import path -from .views import OpenPositions, TrackPositions +from .views import OpenPositions, TrackPositions, UpdateOpenPositions app_name = "kucoin" urlpatterns = [ path('open_positions/', OpenPositions.as_view(), name='open_positions'), # see list of current open positions + path('open_positions/update/', UpdateOpenPositions.as_view(), + name='update_open_positions'), # update list of current open positions path('tracking/', TrackPositions.as_view(), name='tracking'), # enable or disable tracking position ] diff --git a/kucoin/utils.py b/kucoin/utils.py index 4ebffd6..df0c888 100644 --- a/kucoin/utils.py +++ b/kucoin/utils.py @@ -7,6 +7,7 @@ import requests from accounts.encryption import decrypt from django_celery_beat.models import PeriodicTask, IntervalSchedule +from .models import Order schedule, created = IntervalSchedule.objects.get_or_create(every=30, period=IntervalSchedule.SECONDS,) @@ -46,6 +47,10 @@ def kucoin_api(key, secret, passphrase, endpoint) -> tuple: def create_or_delete_celery_task(user, track): + ''' + Create or delete celery task based on track field + ''' + if track: PeriodicTask.objects.get_or_create(interval=schedule, name=f"User({user.pk})", task='kucoin.tasks.tracking_position_per_user', @@ -56,3 +61,30 @@ def create_or_delete_celery_task(user, track): PeriodicTask.objects.filter(name=f"User({user.pk})").delete() return {'message': 'Tracking Disabled'} + + +def update_orders(user): + ''' + Update order objects for user + ''' + + status, response = kucoin_api(user.kucoin_key, user.kucoin_secret, + user.kucoin_passphrase, '/api/v1/orders') + + if response.get('code') == '200000': + items = response['items'] + + for item in items: + Order.objects.update_or_create( + user=user, clientOid=item['clientOid'], + side=item['side'], + symbol=item['symbol'], + type=item['type'], + remark=item['remark'], + stp=item['stp'], + tradeType=item['tradeType'], isActive=item['isActive']) + + if status == 200: + return {'message': 'Open position list updated successfully'}, 200 + + return {'message': 'Connection error'}, 400 diff --git a/kucoin/views.py b/kucoin/views.py index 02da6a1..58d0ee2 100644 --- a/kucoin/views.py +++ b/kucoin/views.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from .serializers import OrderSerializers, TrackSerializers -from .utils import create_or_delete_celery_task +from .utils import create_or_delete_celery_task, update_orders class OpenPositions(GenericAPIView): @@ -38,3 +38,17 @@ def post(self, request): data = create_or_delete_celery_task(user, track) return Response(data, status=200) + + +class UpdateOpenPositions(GenericAPIView): + ''' + Update manually open (active) positions list. + ''' + + permission_classes = (IsAuthenticated,) + + def post(self, request): + user = request.user + data, status = update_orders(user) + + return Response(data, status=status) From 68eb1cdee29a9ba0fc47cb3869a5a07e0a318270 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 17:39:27 +0330 Subject: [PATCH 10/19] Fix update_orders bug --- kucoin/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kucoin/utils.py b/kucoin/utils.py index df0c888..f413ae5 100644 --- a/kucoin/utils.py +++ b/kucoin/utils.py @@ -72,7 +72,7 @@ def update_orders(user): user.kucoin_passphrase, '/api/v1/orders') if response.get('code') == '200000': - items = response['items'] + items = response['data']['items'] for item in items: Order.objects.update_or_create( From eca84e920729316a1a5dc481cab47a3b5a9a9440 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 18:00:24 +0330 Subject: [PATCH 11/19] Remove UpdateOpenPositions --- kucoin/urls.py | 8 ++++---- kucoin/utils.py | 5 ----- kucoin/views.py | 18 ++++++------------ 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/kucoin/urls.py b/kucoin/urls.py index 048c414..8d4b2ca 100644 --- a/kucoin/urls.py +++ b/kucoin/urls.py @@ -1,11 +1,11 @@ from django.urls import path -from .views import OpenPositions, TrackPositions, UpdateOpenPositions +from .views import OpenPositions, TrackPositions app_name = "kucoin" urlpatterns = [ path('open_positions/', OpenPositions.as_view(), name='open_positions'), # see list of current open positions - path('open_positions/update/', UpdateOpenPositions.as_view(), - name='update_open_positions'), # update list of current open positions - path('tracking/', TrackPositions.as_view(), name='tracking'), # enable or disable tracking position + + # enable or disable tracking position & see list of tracking positions + path('tracking_positions/', TrackPositions.as_view(), name='tracking_positions'), ] diff --git a/kucoin/utils.py b/kucoin/utils.py index f413ae5..623753b 100644 --- a/kucoin/utils.py +++ b/kucoin/utils.py @@ -83,8 +83,3 @@ def update_orders(user): remark=item['remark'], stp=item['stp'], tradeType=item['tradeType'], isActive=item['isActive']) - - if status == 200: - return {'message': 'Open position list updated successfully'}, 200 - - return {'message': 'Connection error'}, 400 diff --git a/kucoin/views.py b/kucoin/views.py index 58d0ee2..e331048 100644 --- a/kucoin/views.py +++ b/kucoin/views.py @@ -16,6 +16,7 @@ class OpenPositions(GenericAPIView): def get(self, request) -> Response: user = request.user + update_orders(user) orders = Order.objects.filter(user=user, isActive=True) data = OrderSerializers(orders, many=True).data @@ -24,7 +25,7 @@ def get(self, request) -> Response: class TrackPositions(GenericAPIView): ''' - Enable or disable Tracking positions for each user + Enable or disable tracking positions for each user & see list of tracking positions ''' permission_classes = (IsAuthenticated,) @@ -39,16 +40,9 @@ def post(self, request): return Response(data, status=200) - -class UpdateOpenPositions(GenericAPIView): - ''' - Update manually open (active) positions list. - ''' - - permission_classes = (IsAuthenticated,) - - def post(self, request): + def get(self, request) -> Response: user = request.user - data, status = update_orders(user) + orders = Order.objects.filter(user=user) + data = OrderSerializers(orders, many=True).data - return Response(data, status=status) + return Response(data, status=200) From 7ae98e7125b4f8cc02ede8ebc8650266fb31d02c Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 18:07:38 +0330 Subject: [PATCH 12/19] Change response if it was empty --- kucoin/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kucoin/views.py b/kucoin/views.py index e331048..9985b27 100644 --- a/kucoin/views.py +++ b/kucoin/views.py @@ -18,7 +18,7 @@ def get(self, request) -> Response: user = request.user update_orders(user) orders = Order.objects.filter(user=user, isActive=True) - data = OrderSerializers(orders, many=True).data + data = OrderSerializers(orders, many=True).data or 'Empty' return Response(data, status=200) @@ -43,6 +43,6 @@ def post(self, request): def get(self, request) -> Response: user = request.user orders = Order.objects.filter(user=user) - data = OrderSerializers(orders, many=True).data + data = OrderSerializers(orders, many=True).data or 'Empty' return Response(data, status=200) From a9a49336c7a74d48e17c8a9604a71797878e54d0 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 18:52:32 +0330 Subject: [PATCH 13/19] Update README.md --- README.md | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1fb5bf9..5a17839 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,16 @@ -# In name of Allah +# Documentation +You can check full documentation from [here]() ## Introduction -We want to develop a REST based django application to read signed in user's KuCoin open positions for them. For this to happen, you must be able to read user's positions (if requested from user) every 30 seconds. +This project is a REST based django application to read signed in user's KuCoin open positions for them. For this to happen, application able to read user's positions (if requested from user) every 30 seconds. -The application you develop must have these features: -- Users should be able to **sign up** (provide name, username & kucoin details). -- Users should be able to **sign in**. -- Users should be able to **request position tracking**. -- Users should be able to **see list** of current open positions. -- Application should be able to track user's positions every 30 seconds. -- Application should be able to handle multi users. -- Application must be able to resume its job after restart. +Application have these features: +- Users able to **sign up** (provide name, username & kucoin details). +- Users able to **sign in** with jwt authentication. +- Users able to **request position tracking** with send request based on **track** field ("true" or "false"). +- Users able to **see list** of current open (active) positions. +- Application able to track user's positions every 30 seconds with celery periodic task. +- Application able to handle multi users with celery task & redis. +- Application able to resume its job after restart with redis backend and database. -You can use [this api](https://docs.kucoin.com/#list-accounts) from KuCoin to handle this. You should create KuCoin account if you do not have already & get api key & secret to test your implementation. Note we do not need your api key, that is only for your own usage. - -### Note -- We do **NOT want any kind of UI** from you -- KuCoin api key & secrets should not be stored in raw format - - -## Expectations: -We want a clean, readable and maintainable code with meaningful comments and docstrings. Also you need to provide postman API doc for web app. - -## Task -1. Fork this repository -2. Develop the challenge with Django 3 or higher -3. Push your code to your repository -4. Send us a pull request, we will review and get back to you -5. Enjoy +Application use [this api](https://docs.kucoin.com/#list-accounts) from KuCoin to handle KuCoin positions. From a8c64a069de9f4654bb25e9bd604178366692a07 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 19:27:06 +0330 Subject: [PATCH 14/19] Add runner.sh + Change celery settings --- crypto_reader/settings.py | 8 ++++++-- runner.sh | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100755 runner.sh diff --git a/crypto_reader/settings.py b/crypto_reader/settings.py index bb813de..13eb397 100644 --- a/crypto_reader/settings.py +++ b/crypto_reader/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/4.1/ref/settings/ """ +import os from pathlib import Path from datetime import timedelta @@ -142,11 +143,14 @@ STATIC_URL = 'static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static/') + # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # celery settings -CELERY_BROKER_URL = 'redis://localhost:6379/0' -CELERY_RESULT_BACKEND = 'redis://localhost:6379/0' +# SECURITY WARNING: don't use raw password in production! +CELERY_BROKER_URL = 'redis://default:blR9KZ39lE6cBf8JraZsVtbpU1Bruh3U@redis-18003.c275.us-east-1-4.ec2.cloud.redislabs.com:18003' +CELERY_RESULT_BACKEND = 'redis://default:blR9KZ39lE6cBf8JraZsVtbpU1Bruh3U@redis-18003.c275.us-east-1-4.ec2.cloud.redislabs.com:18003' diff --git a/runner.sh b/runner.sh new file mode 100755 index 0000000..78f9b32 --- /dev/null +++ b/runner.sh @@ -0,0 +1,12 @@ +echo "Makemigrations and migrate..." +python3 manage.py makemigrations --noinput +python3 manage.py migrate --noinput + +echo "Collectstatic..." +python3 manage.py collectstatic --noinput + +echo "Start celery worker..." +celery -A crypto_reader worker --beat --scheduler django --loglevel=info & + +echo "Runserver" +python3 manage.py runserver From ab09c881251283edfd8c76adf91aab7d59dfae43 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 19:55:52 +0330 Subject: [PATCH 15/19] Add Note to README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a17839..77f8d54 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # Documentation + You can check full documentation from [here]() ## Introduction -This project is a REST based django application to read signed in user's KuCoin open positions for them. For this to happen, application able to read user's positions (if requested from user) every 30 seconds. + +This project is a REST based django application to read signed in user's KuCoin open positions for them. For this to happen, application able to read user's positions (if requested from user) every 30 seconds. Application have these features: + - Users able to **sign up** (provide name, username & kucoin details). - Users able to **sign in** with jwt authentication. - Users able to **request position tracking** with send request based on **track** field ("true" or "false"). @@ -14,3 +17,7 @@ Application have these features: - Application able to resume its job after restart with redis backend and database. Application use [this api](https://docs.kucoin.com/#list-accounts) from KuCoin to handle KuCoin positions. + +# Note + +**Attention!** All settings such as SECRET_KEY, DEBUG, ENCRYPT_KEY and etc have been exhibited because this is a test project, please keep those in production secret. From daf309e825fc0900c629fa0fe926fb080b5eb4c6 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 21:25:48 +0330 Subject: [PATCH 16/19] Change runner.sh --- runner.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/runner.sh b/runner.sh index 78f9b32..06dab73 100755 --- a/runner.sh +++ b/runner.sh @@ -1,3 +1,16 @@ +#!/bin/bash + +echo "Create and activated venv..." +pip install virtualenv +python3 -m virtualenv venv +if [ "$OSTYPE" == "linux-gnu" ]; then +source venv/bin/activate +elif [[ "$OSTYPE" == "cygwin" || "$OSTYPE" == "msys" ]]; then +.\venv\Scripts\activate.bat +fi +pip install --upgrade pip +pip install -r requirements.txt + echo "Makemigrations and migrate..." python3 manage.py makemigrations --noinput python3 manage.py migrate --noinput @@ -5,7 +18,7 @@ python3 manage.py migrate --noinput echo "Collectstatic..." python3 manage.py collectstatic --noinput -echo "Start celery worker..." +echo "Start celery worker in background..." celery -A crypto_reader worker --beat --scheduler django --loglevel=info & echo "Runserver" From c4484fbf3ceacbe85ff4050ae4dccf3defb9f2d8 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 21:56:31 +0330 Subject: [PATCH 17/19] Fix django_celery_beat bug --- kucoin/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kucoin/utils.py b/kucoin/utils.py index 623753b..2f0a670 100644 --- a/kucoin/utils.py +++ b/kucoin/utils.py @@ -9,8 +9,6 @@ from django_celery_beat.models import PeriodicTask, IntervalSchedule from .models import Order -schedule, created = IntervalSchedule.objects.get_or_create(every=30, period=IntervalSchedule.SECONDS,) - def kucoin_api(key, secret, passphrase, endpoint) -> tuple: ''' @@ -51,6 +49,8 @@ def create_or_delete_celery_task(user, track): Create or delete celery task based on track field ''' + schedule, created = IntervalSchedule.objects.get_or_create(every=30, period=IntervalSchedule.SECONDS,) + if track: PeriodicTask.objects.get_or_create(interval=schedule, name=f"User({user.pk})", task='kucoin.tasks.tracking_position_per_user', From 8c60de7c8fc2ee585896ea9598d0295da1812692 Mon Sep 17 00:00:00 2001 From: HamidReza Date: Fri, 7 Oct 2022 22:18:19 +0330 Subject: [PATCH 18/19] Add documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77f8d54..33f726f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Documentation -You can check full documentation from [here]() +You can check full documentation with postman from [here](https://documenter.getpostman.com/view/17658108/2s83zgu4om) ## Introduction From d38183b35b3ca6694221bfa51692543230817b2a Mon Sep 17 00:00:00 2001 From: HamidReza Date: Sat, 8 Oct 2022 11:15:20 +0330 Subject: [PATCH 19/19] Add comments --- accounts/admin.py | 4 ++++ crypto_reader/urls.py | 6 +++--- kucoin/admin.py | 4 ++++ kucoin/models.py | 4 ++++ kucoin/utils.py | 7 +++++-- kucoin/views.py | 4 ++-- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/accounts/admin.py b/accounts/admin.py index 0d31c30..4089163 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -5,6 +5,10 @@ @admin.register(User) class UserAdmin(DefaultUserAdmin): + ''' + Change field of default user admin page + ''' + fieldsets = ( (None, {'fields': ('username', 'password')}), ('Personal info', diff --git a/crypto_reader/urls.py b/crypto_reader/urls.py index 7d88988..d5bb9d4 100644 --- a/crypto_reader/urls.py +++ b/crypto_reader/urls.py @@ -17,7 +17,7 @@ from django.urls import path, include urlpatterns = [ - path('admin/', admin.site.urls), - path('accounts/', include('accounts.urls')), - path('kucoin/', include('kucoin.urls')), + path('admin/', admin.site.urls), # admin page + path('accounts/', include('accounts.urls')), # accounts app + path('kucoin/', include('kucoin.urls')), # kucoin app ] diff --git a/kucoin/admin.py b/kucoin/admin.py index 3204b1c..55e8b07 100644 --- a/kucoin/admin.py +++ b/kucoin/admin.py @@ -4,6 +4,10 @@ @admin.register(Order) class OrderAdmin(admin.ModelAdmin): + ''' + Register and change Order admin page + ''' + list_filter = ('isActive',) list_display = ('user', 'clientOid', 'isActive') list_editable = ('isActive',) diff --git a/kucoin/models.py b/kucoin/models.py index da784b5..ce22d4e 100644 --- a/kucoin/models.py +++ b/kucoin/models.py @@ -3,6 +3,10 @@ class Order(models.Model): + ''' + save kucoin orders as objects of Order model + ''' + user = models.ForeignKey(User, on_delete=models.CASCADE) clientOid = models.CharField(max_length=200) side = models.CharField(max_length=200) diff --git a/kucoin/utils.py b/kucoin/utils.py index 2f0a670..94ced94 100644 --- a/kucoin/utils.py +++ b/kucoin/utils.py @@ -32,6 +32,7 @@ def kucoin_api(key, secret, passphrase, endpoint) -> tuple: passphrase.encode('utf-8'), hashlib.sha256).digest()) + # create headers for requests headers = { "KC-API-SIGN": signature, "KC-API-TIMESTAMP": str(now), @@ -44,7 +45,7 @@ def kucoin_api(key, secret, passphrase, endpoint) -> tuple: return response.status_code, response.json() -def create_or_delete_celery_task(user, track): +def create_or_delete_celery_task(user, track) -> dict: ''' Create or delete celery task based on track field ''' @@ -65,15 +66,17 @@ def create_or_delete_celery_task(user, track): def update_orders(user): ''' - Update order objects for user + Update order objects for user. Request to kucoin api and get and save in database. ''' + # request to kucoin status, response = kucoin_api(user.kucoin_key, user.kucoin_secret, user.kucoin_passphrase, '/api/v1/orders') if response.get('code') == '200000': items = response['data']['items'] + # Save list as objects of Order model for item in items: Order.objects.update_or_create( user=user, clientOid=item['clientOid'], diff --git a/kucoin/views.py b/kucoin/views.py index 9985b27..c48c054 100644 --- a/kucoin/views.py +++ b/kucoin/views.py @@ -8,7 +8,7 @@ class OpenPositions(GenericAPIView): ''' - Get and view open (active) positions. + Get and view open (active) positions. Orders save as objects in database with Order model and querying to get those. ''' permission_classes = (IsAuthenticated,) @@ -25,7 +25,7 @@ def get(self, request) -> Response: class TrackPositions(GenericAPIView): ''' - Enable or disable tracking positions for each user & see list of tracking positions + Enable or disable tracking positions for each user & see list of tracking positions. enable/disable tracking using create/delete celery task. Get list of tracking positions using querying Order model. ''' permission_classes = (IsAuthenticated,)