diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf43b00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +__pycache__ +*.swp +*.env +.vscode +*.sqlite +static/* \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/active_dev.sh b/active_dev.sh new file mode 100644 index 0000000..d0a65e0 --- /dev/null +++ b/active_dev.sh @@ -0,0 +1,5 @@ +set -o allexport +. ./config/gunicorn.dev.env +. ./config/django.dev.env +. ./config/postgres.dev.env +set +o allexport \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..cfa37a6 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for CryptoReader 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.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + +application = get_asgi_application() diff --git a/config/django.dev.env b/config/django.dev.env new file mode 100644 index 0000000..5aa58fe --- /dev/null +++ b/config/django.dev.env @@ -0,0 +1,8 @@ +# DJANGO CONFIG +REQUIREMENTS_FILE=/src/requirements/dev.txt # This Env Required For Building Dockerfile of Djanog Service +DJANGO_SETTINGS_MODULE=config.settings.dev +SECRET_KEY="i0cwcl!$e54^#0vh6q^)$!+l=_at##a%#0q2&!g$$dau*d2rg8" +ENCRYPT_KEY="f7DwTK5K6EwmA30THAxnXgBKy_v969ItANlVGhi4C-0=" +ALLOWED_HOSTS="* localhost 127.0.0.1" +DEBUG=True # Default : False +APPEND_SLASH=True # Default : True \ No newline at end of file diff --git a/config/gunicorn.dev.env b/config/gunicorn.dev.env new file mode 100644 index 0000000..31c6532 --- /dev/null +++ b/config/gunicorn.dev.env @@ -0,0 +1,7 @@ +# Django & Nginx Service +DJANGO_IP="0.0.0.0" +DJANGO_PORT=8181 + +# gunicorn Service +WORKERS_COUNT=2 +EXTRA_ARGS="" \ No newline at end of file diff --git a/config/nginx.conf b/config/nginx.conf new file mode 100644 index 0000000..92d6ed3 --- /dev/null +++ b/config/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_redirect off; + proxy_buffering off; + proxy_pass http://django; + } + + location /static { + # path for static files + alias /statics/; + } + } + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream django { + server django:${DJANGO_PORT}; + } \ No newline at end of file diff --git a/config/postgres.dev.env b/config/postgres.dev.env new file mode 100644 index 0000000..6ee04ca --- /dev/null +++ b/config/postgres.dev.env @@ -0,0 +1,4 @@ +# POSTGRES CONFIG +POSTGRES_DB=djangoCryptoReader +POSTGRES_USER=django +POSTGRES_PASSWORD=django-password diff --git a/config/settings/__init__.py b/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/settings/base.py b/config/settings/base.py new file mode 100644 index 0000000..3b79573 --- /dev/null +++ b/config/settings/base.py @@ -0,0 +1,123 @@ +""" +Django settings for CryptoReader project. + +Generated by 'django-admin startproject' using Django 4.0.5. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-s#0r5^_%a3-4ts9ug==i4^xy&x2py@hq3_i+!k@2^i)l-v@c=9" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +AUTH_USER_MODEL = "accounts.User" +# 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 = "config.urls.dev" + +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 = "config.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.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/4.0/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.0/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/config/settings/dev.py b/config/settings/dev.py new file mode 100644 index 0000000..a298486 --- /dev/null +++ b/config/settings/dev.py @@ -0,0 +1,73 @@ +from config.settings.base import * + +import environ +import os + +env = environ.Env() +environ.Env.read_env(os.path.join(BASE_DIR / "config/django.dev.env")) + +SECRET_KEY = env("SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env("DEBUG") + +ALLOWED_HOSTS = str(env("ALLOWED_HOSTS")).strip('"').strip().split() +APPEND_SLASH = env("APPEND_SLASH") + +# encrypt key for encrypting and decrypting data +# generated with cryptography.fernet.Fernet.generate_key() +ENCRYPT_KEY = env("ENCRYPT_KEY") +# ENCRYPT_KEY = b"f7DwTK5K6EwmA30THAxnXgBKy_v969ItANlVGhi4C-0=" + +INSTALLED_APPS += [ + "rest_framework", + "drf_spectacular", + "drf_spectacular_sidecar", + "cryptoreader.accounts", + "cryptoreader.api", +] + + +ROOT_URLCONF = "config.urls.dev" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": env("POSTGRES_DB"), + "USER": env("POSTGRES_USER"), + "PASSWORD": env("POSTGRES_PASSWORD"), + "HOST": "postgres", + "PORT": "5432", + } +} + +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://redis:6379/1", + "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, + "KEY_PREFIX": "django_", + } +} + +REST_FRAMEWORK = { + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework_simplejwt.authentication.JWTAuthentication", + ], +} + +SPECTACULAR_SETTINGS = { + "TITLE": "Crypto Rader ", + "DESCRIPTION": "Crypot Reader Challnge From Nilva Co", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, + "SWAGGER_UI_DIST": "SIDECAR", + "SWAGGER_UI_FAVICON_HREF": "SIDECAR", + "REDOC_DIST": "SIDECAR", +} + +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "static" +STATICFILES_DIRS = [] diff --git a/config/urls/__init__.py b/config/urls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/urls/base.py b/config/urls/base.py new file mode 100644 index 0000000..df42ce7 --- /dev/null +++ b/config/urls/base.py @@ -0,0 +1,21 @@ +"""CryptoReader URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.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 path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/config/urls/dev.py b/config/urls/dev.py new file mode 100644 index 0000000..5b71d9a --- /dev/null +++ b/config/urls/dev.py @@ -0,0 +1,27 @@ +from django.urls import include +from config.urls.base import * + +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerView, +) + + +urlpatterns += [ + path( + "api/v1/", + include(("cryptoreader.api.urls", "API/V1")), + ), + path("api/v1/schema/", SpectacularAPIView.as_view(), name="schema"), + path( + "api/v1/schema/swagger-ui/", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), + path( + "api/v1/schema/redoc/", + SpectacularRedocView.as_view(url_name="schema"), + name="redoc", + ), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..d626c2f --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for CryptoReader 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.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + +application = get_wsgi_application() diff --git a/cryptoreader/__init__.py b/cryptoreader/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoreader/__pycache__/__init__.cpython-39.pyc b/cryptoreader/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..948b676 Binary files /dev/null and b/cryptoreader/__pycache__/__init__.cpython-39.pyc differ diff --git a/cryptoreader/accounts/__init__.py b/cryptoreader/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoreader/accounts/admin.py b/cryptoreader/accounts/admin.py new file mode 100644 index 0000000..f1fbd74 --- /dev/null +++ b/cryptoreader/accounts/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from .models import KucoinAccount, User + +# Register your models here. +class KucoinAccountModelAdmin(admin.ModelAdmin): + list_display = [ + "user", + "type", + "currency", + "balance", + "available", + "holds", + ] + + +admin.site.register(KucoinAccount, KucoinAccountModelAdmin) +admin.site.register(User) diff --git a/cryptoreader/accounts/apps.py b/cryptoreader/accounts/apps.py new file mode 100644 index 0000000..7831b5a --- /dev/null +++ b/cryptoreader/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "cryptoreader.accounts" diff --git a/cryptoreader/accounts/migrations/0001_initial.py b/cryptoreader/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..2ef8918 --- /dev/null +++ b/cryptoreader/accounts/migrations/0001_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 4.0.6 on 2022-07-20 08:25 + +from django.conf import settings +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +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')), + ('key', models.CharField(max_length=248)), + ('secret', models.CharField(max_length=248)), + ('passphrase', models.CharField(max_length=558)), + ('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, + }, + ), + migrations.CreateModel( + name='KucoinAccount', + fields=[ + ('pk_uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('id', models.CharField(max_length=24)), + ('type', models.CharField(choices=[('main', 'Main'), ('trade', 'Trade'), ('margain', 'Margin')], default='main', max_length=7)), + ('currency', models.CharField(max_length=24)), + ('balance', models.DecimalField(decimal_places=10, max_digits=20)), + ('available', models.DecimalField(decimal_places=10, max_digits=20)), + ('holds', models.DecimalField(decimal_places=10, max_digits=20)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/cryptoreader/accounts/migrations/0002_alter_user_managers_alter_user_is_active.py b/cryptoreader/accounts/migrations/0002_alter_user_managers_alter_user_is_active.py new file mode 100644 index 0000000..015560e --- /dev/null +++ b/cryptoreader/accounts/migrations/0002_alter_user_managers_alter_user_is_active.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.6 on 2022-07-21 00:25 + +import cryptoreader.accounts.models +import django.contrib.auth.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AlterModelManagers( + name='user', + managers=[ + ('objects', cryptoreader.accounts.models.CustomUserManager()), + ('_objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.AlterField( + model_name='user', + name='is_active', + field=models.BooleanField(default=True), + ), + ] diff --git a/cryptoreader/accounts/migrations/__init__.py b/cryptoreader/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoreader/accounts/models.py b/cryptoreader/accounts/models.py new file mode 100644 index 0000000..e738d16 --- /dev/null +++ b/cryptoreader/accounts/models.py @@ -0,0 +1,96 @@ +from uuid import uuid4 +from django.db import models +from django.contrib.auth import get_user_model +from django.core.exceptions import FieldError +from . import tools +from django.contrib.auth.models import AbstractUser, UserManager + + +class CustomUserManager(UserManager): + def create(self, **kwargs): + """encrtept data""" + are_args_complete = all( + [ + kwargs.setdefault("key", False), + kwargs.setdefault("secret", False), + kwargs.setdefault("passphrase", False), + ], + ) + + if not are_args_complete: + raise FieldError("pass all args [ key, secret, passphrase ]") + + kwargs["key"] = tools.encrypt(kwargs["key"]) + kwargs["secret"] = tools.encrypt(kwargs["secret"]) + kwargs["passphrase"] = tools.encrypt(kwargs["passphrase"]) + + return super().create(**kwargs) + + +class User(AbstractUser): + key = models.CharField(max_length=248) + secret = models.CharField(max_length=248) + passphrase = models.CharField(max_length=558) + is_active = models.BooleanField(default=True) + + # change the default model manager + objects = CustomUserManager() + _objects = UserManager() + + def __str__(self) -> str: + return str(self.username) + + def set_key(self, key): + self.key = tools.encrypt(key) + + def get_key(self): + return tools.decrypt(self.key) + + def set_secret(self, secret): + self.secret = tools.encrypt(secret) + + def get_secret(self): + return tools.decrypt(self.secret) + + def set_passphrase(self, passphrase): + self.passphrase = tools.encrypt(passphrase) + + def get_passphrase(self): + return tools.decrypt(self.passphrase) + + +class KucoinAccount(models.Model): + class TypeAccount(models.TextChoices): + main = "main", "Main" + trade = "trade", "Trade" + margin = "margain", "Margin" + + pk_uuid = models.UUIDField( + primary_key=True, + editable=False, + default=uuid4, + ) + + user = models.ForeignKey( + User, + models.CASCADE, + ) + id = models.CharField(max_length=24) # accountId + type = models.CharField( + max_length=7, + choices=TypeAccount.choices, + default=TypeAccount.main, + ) + currency = models.CharField(max_length=24) # Currency + balance = models.DecimalField( + max_digits=20, decimal_places=10 + ) # Total assets of a currency + available = models.DecimalField( + max_digits=20, decimal_places=10 + ) # Available assets of a currency + holds = models.DecimalField( + max_digits=20, decimal_places=10 + ) # Hold assets of a currency + + def __str__(self) -> str: + return str(self.kucoin_api) diff --git a/cryptoreader/accounts/serializers.py b/cryptoreader/accounts/serializers.py new file mode 100644 index 0000000..0571071 --- /dev/null +++ b/cryptoreader/accounts/serializers.py @@ -0,0 +1,33 @@ +from rest_framework import serializers +from .models import User, KucoinAccount +from django.contrib.auth.hashers import make_password + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + "username", + "password", + "email", + "first_name", + "last_name", + "key", + "secret", + "passphrase", + ] + + def create(self, validated_data): + validated_data["password"] = make_password(validated_data["password"]) + return super().create(validated_data) + + +class KucoinAccountSerializer(serializers.ModelSerializer): + user = serializers.HiddenField(default=serializers.CurrentUserDefault()) + + class Meta: + model = KucoinAccount + exclude = ["pk_uuid"] + + def validate(self, attrs): + return super().validate(attrs) diff --git a/cryptoreader/accounts/tests.py b/cryptoreader/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cryptoreader/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cryptoreader/accounts/tools.py b/cryptoreader/accounts/tools.py new file mode 100644 index 0000000..91fb88d --- /dev/null +++ b/cryptoreader/accounts/tools.py @@ -0,0 +1,26 @@ +import base64 + +from cryptography.fernet import Fernet + +from django.conf import settings + + +def encrypt(pas): + try: + pas = str(pas) + cipher_pass = Fernet(settings.ENCRYPT_KEY) + encrypt_pass = cipher_pass.encrypt(pas.encode("ascii")) + encrypt_pass = base64.urlsafe_b64encode(encrypt_pass).decode("ascii") + return encrypt_pass + except Exception as e: + return None + + +def decrypt(pas): + try: + pas = base64.urlsafe_b64decode(pas) + cipher_pass = Fernet(settings.ENCRYPT_KEY) + decod_pass = cipher_pass.decrypt(pas).decode("ascii") + return decod_pass + except Exception as e: + return None diff --git a/cryptoreader/accounts/views.py b/cryptoreader/accounts/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/cryptoreader/accounts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/cryptoreader/api/__init__.py b/cryptoreader/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoreader/api/admin.py b/cryptoreader/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/cryptoreader/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/cryptoreader/api/apps.py b/cryptoreader/api/apps.py new file mode 100644 index 0000000..2fdd32a --- /dev/null +++ b/cryptoreader/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "cryptoreader.api" diff --git a/cryptoreader/api/migrations/__init__.py b/cryptoreader/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cryptoreader/api/models.py b/cryptoreader/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/cryptoreader/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/cryptoreader/api/tests.py b/cryptoreader/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cryptoreader/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cryptoreader/api/urls.py b/cryptoreader/api/urls.py new file mode 100644 index 0000000..b0e94b5 --- /dev/null +++ b/cryptoreader/api/urls.py @@ -0,0 +1,22 @@ +from email.mime import base +from django.urls import path, include +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, +) +from rest_framework import routers +from cryptoreader.api.views import KucoinAccountViewSet, UserViewSet + +router = routers.DefaultRouter() +router.register("kucoinaccount", KucoinAccountViewSet) +router.register("user", UserViewSet, "user") + + +app_name = "API/V1" +urlpatterns = [ + path("", include(router.urls)), + # path("user", UserCreate.as_view(), name="user-create"), + # path("user2", UserDetail.as_view(), name="user-detail"), + path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), +] diff --git a/cryptoreader/api/views.py b/cryptoreader/api/views.py new file mode 100644 index 0000000..83f2bac --- /dev/null +++ b/cryptoreader/api/views.py @@ -0,0 +1,205 @@ +from cryptoreader.accounts.serializers import ( + KucoinAccount, + KucoinAccountSerializer, + User, + UserSerializer, +) +from django.http import Http404 +from drf_spectacular.utils import extend_schema +from rest_framework import permissions, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework import status + +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers +from third_party_sdks.sdk_api import fetch_accounts + +# Raise an error in the endpoint for invalid data +RAISE_ERROR_IF_INVALID = True +CACHE_TTL_ACCOUNTS = 30 + + +def load_accounts_from_kucoin_by_user(user: User) -> dict: + login_info = user.get_key(), user.get_secret(), user.get_passphrase() + accounts = fetch_accounts(*login_info) + return accounts + + +class UserViewSet(viewsets.ViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + + def get_permissions(self): + """ + Instantiates and returns the list of permissions that this view requires. + """ + if self.action == "create": + permission_classes = [permissions.AllowAny] + elif self.action in ["me", "update_me", "destroy_me"]: + permission_classes = [permissions.IsAuthenticated] + else: + permission_classes = [permissions.IsAdminUser] + + return [permission() for permission in permission_classes] + + def get_object(self): + return self.request.user + + @extend_schema( + summary="get current user detail", + parameters=None, + responses={ + 200: KucoinAccountSerializer(many=True), + 404: None, + }, + ) + @action(methods=["GET"], detail=False) + @method_decorator(cache_page(60 * 60 * 24)) + @method_decorator( + vary_on_headers( + "Authorization", + ) + ) + def me(self, request): + user = self.get_object() + serializer = UserSerializer(user) + return Response(serializer.data, status.HTTP_200_OK) + + @extend_schema( + summary="sign-up new user", + parameters=None, + request=UserSerializer, + responses={ + 200: UserSerializer(many=True), + 404: None, + }, + ) + def create(self, request): + serializer = UserSerializer(data=request.data) + + if serializer.is_valid(RAISE_ERROR_IF_INVALID): + serializer.save() + return Response(serializer.data, status.HTTP_201_CREATED) + + @extend_schema( + summary="updating current user", + request=UserSerializer, + responses={ + 200: UserSerializer(), + 404: None, + }, + ) + @me.mapping.put + def update_me(self, request, partial=False): + user = self.get_object() + serializer = UserSerializer(user, request.data, partial=partial) + if serializer.is_valid(RAISE_ERROR_IF_INVALID): + serializer.save() + return Response(serializer.data, status.HTTP_200_OK) + + @extend_schema( + summary="deleting current user", + request=UserSerializer, + responses={ + 404: None, + }, + ) + @me.mapping.delete + def destroy_me(self, request): + user = self.get_object() + user.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class KucoinAccountViewSet(viewsets.ViewSet): + queryset = KucoinAccount.objects.all() + serializer_class = KucoinAccountSerializer + lookup_field = "id" + permission_classes = (permissions.IsAuthenticated,) + + def get_serializer_context(self): + context = super().get_serializer_context() + context.update({"request": self.request}) + return context + + def get_object(self, id): + account = KucoinAccount.objects.filter(user=self.request.user, id=id).last() + if account: + return account + raise Http404 + + @extend_schema( + summary="create a new account", + request=KucoinAccountSerializer, + responses={ + 200: KucoinAccountSerializer, + 404: None, + }, + ) + def create(self, request, *args, **kwargs): + serializer = KucoinAccountSerializer( + data=request.data, context={"request": request} + ) + if serializer.is_valid(RAISE_ERROR_IF_INVALID): + serializer.save() + return Response(serializer.data, status.HTTP_201_CREATED) + + @extend_schema( + summary="get the list of accounts specified with the ID ", + responses={ + 200: KucoinAccountSerializer, + 404: None, + }, + ) + def retrieve(self, request, *args, **kwargs): + account = self.get_object(kwargs.setdefault("id", None)) + serializer = KucoinAccountSerializer(account) + return Response(serializer.data, status.HTTP_200_OK) + + @extend_schema( + summary="get the all available accounts", + responses={ + 200: KucoinAccountSerializer(many=True), + 404: None, + }, + ) + @method_decorator(cache_page(CACHE_TTL_ACCOUNTS)) + @method_decorator( + vary_on_headers( + "Authorization", + ) + ) + def list(self, request, *args, **kwargs): + user = user = self.request.user + accounts = KucoinAccount.objects.filter(user=user) + serializer = KucoinAccountSerializer(accounts, many=True) + return Response(serializer.data, status.HTTP_200_OK) + + @extend_schema( + summary="get the open accounts", + responses={ + 200: KucoinAccountSerializer(many=True), + 404: None, + }, + ) + @action(detail=False, methods=("GET",)) + @method_decorator(cache_page(CACHE_TTL_ACCOUNTS)) + @method_decorator( + vary_on_headers( + "Authorization", + ) + ) + def list_current(self, request, *args, **kwargs): + """fetch last avilable accounts form kucoin and instert to db""" + + user = self.request.user + accounts = load_accounts_from_kucoin_by_user(user) + serializer = KucoinAccountSerializer( + data=accounts, many=True, context={"request": request} + ) + + if serializer.is_valid(RAISE_ERROR_IF_INVALID): + serializer.save() + return Response(serializer.data, status.HTTP_200_OK) diff --git a/django-manager.sh b/django-manager.sh new file mode 100644 index 0000000..82de7b4 --- /dev/null +++ b/django-manager.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +args=$@ +docker compose exec django ./manage.py $args \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..a555d28 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,44 @@ +services: + + django: + volumes: + - .:/src/:rw + - statics:/src/static:rw + env_file: + - ./config/gunicorn.dev.env + - ./config/django.dev.env + - ./config/postgres.dev.env + entrypoint: [ "bash", "./entrypoint.dev.sh" ] + + postgres: + env_file: + - ./config/postgres.dev.env + volumes: + - db-data-test:/var/lib/postgresql/data:rw + + nginx: + depends_on: + - django + env_file: + - ./config/gunicorn.dev.env + ports: + - 8000:80 + volumes: + - statics:/statics:rw + + celery_worker: + env_file: + - ./config/django.dev.env + - ./config/postgres.dev.env + command: celery -A kucoin_updater.tasks worker --loglevel=info + + + celery_beat: + env_file: + - ./config/django.dev.env + - ./config/postgres.dev.env + command: celery -A kucoin_updater.beat beat --loglevel=info + +volumes: + db-data-test: + statics: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c31fe5e --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,47 @@ +version: '3' + +services: + + django: + build: + dockerfile: ./dockerfile + args: + - REQUIREMENTS_FILE=${REQUIREMENTS_FILE} + depends_on: + - redis + - postgres + + + redis: + image: redis:alpine + restart: always + + postgres: + image: postgres:alpine + restart: always + volumes: + - db-data:/var/lib/postgresql/data:rw + + nginx: + build: + dockerfile: ./dockerfile.nginx + restart: always + + celery_beat: + build: + dockerfile: ./dockerfile + args: + - REQUIREMENTS_FILE=${REQUIREMENTS_FILE} + depends_on: + - redis + + celery_worker: + build: + dockerfile: ./dockerfile + args: + - REQUIREMENTS_FILE=${REQUIREMENTS_FILE} + depends_on: + - redis + +volumes: + db-data: \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..0b84004 --- /dev/null +++ b/dockerfile @@ -0,0 +1,14 @@ +######################## BASE PYTHON IMAGE ######################## +FROM python:3.9 + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +ARG REQUIREMENTS_FILE + + +WORKDIR /src +COPY . . + +RUN pip install --upgrade pip +RUN pip install --no-cache-dir -r ${REQUIREMENTS_FILE} \ No newline at end of file diff --git a/dockerfile.nginx b/dockerfile.nginx new file mode 100644 index 0000000..3e169af --- /dev/null +++ b/dockerfile.nginx @@ -0,0 +1,3 @@ +FROM nginx:1.21.6-alpine +RUN rm /etc/nginx/conf.d/default.conf +COPY ./config/nginx.conf /etc/nginx/templates/default.conf.template diff --git a/entrypoint.dev.sh b/entrypoint.dev.sh new file mode 100644 index 0000000..6ed6fc5 --- /dev/null +++ b/entrypoint.dev.sh @@ -0,0 +1,10 @@ +echo -e "\n$(tput bold setaf 6)Makemigrations and apply migrates ... " +python3 manage.py makemigrations --noinput +python3 manage.py migrate --noinput + +echo -e "\n$(tput bold setaf 6)Collect static files ..." +python3 manage.py collectstatic --noinput + + +echo -e "\n$(tput bold setaf 6)Run gunicorn development server ... " +gunicorn --reload -b ${DJANGO_IP}:${DJANGO_PORT} --workers ${WORKERS_COUNT} ${EXTRA_ARGS} config.wsgi:application \ No newline at end of file diff --git a/install-service.dev.sh b/install-service.dev.sh new file mode 100644 index 0000000..5c997b2 --- /dev/null +++ b/install-service.dev.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +#Active all env vars for docker compose files +. ./active_dev.sh + +docker compose -f ./docker-compose.yaml -f docker-compose.dev.yaml up -d --build \ No newline at end of file diff --git a/kucoin_updater/__init__.py b/kucoin_updater/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kucoin_updater/beat.py b/kucoin_updater/beat.py new file mode 100644 index 0000000..5912ba6 --- /dev/null +++ b/kucoin_updater/beat.py @@ -0,0 +1,9 @@ +from kucoin_updater.tasks import app, update_all_user_kucoin_account + +REPEAT_EVERY = 30 + + +@app.on_after_configure.connect +def schedule_periodic_tasks(sender, **kwargs): + # Fetch and updating accounts of kucoin for all users every 30 seconds + sender.add_periodic_task(REPEAT_EVERY, update_all_user_kucoin_account.s()) diff --git a/kucoin_updater/init_django.py b/kucoin_updater/init_django.py new file mode 100644 index 0000000..ce5998c --- /dev/null +++ b/kucoin_updater/init_django.py @@ -0,0 +1,9 @@ +import os +import sys + +import django + +parent_directory = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +sys.path.append(parent_directory) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") +django.setup() diff --git a/kucoin_updater/tasks.py b/kucoin_updater/tasks.py new file mode 100644 index 0000000..66a1f29 --- /dev/null +++ b/kucoin_updater/tasks.py @@ -0,0 +1,66 @@ +from celery import Celery +from celery.utils.log import get_task_logger + +# setup django env +from . import init_django + +from cryptoreader.accounts import models, serializers +from django.http import HttpRequest + +from third_party_sdks.sdk_api import fetch_accounts + +app = Celery("tasks", broker="redis://redis:6379/2") +logger = get_task_logger(__name__) + + +def load_accounts_from_kucoin_by_user(user) -> dict: + login_info = user.get_key(), user.get_secret(), user.get_passphrase() + accounts = fetch_accounts(*login_info) + return accounts + + +@app.task +def update_kucoin_account(username): + """fetch kucoin accounts and insert data + to DB + + Args: + username (str): username of user (Model User) + """ + + try: + user = models.User.objects.get(username=username) + except models.User.DoesNotExist: + logger.debug("user not found") + return + + try: + account = load_accounts_from_kucoin_by_user(user) + except Exception as error: + logger.debug(error) + return + + request = HttpRequest() + request.user = user + context = {"request": request} + serializer = serializers.KucoinAccountSerializer( + data=account, many=True, context=context + ) + + try: + serializer.is_valid(True) + serializer.save() + logger.info(serializer.data) + except Exception as error: + logger.debug(error) + + return None + + +@app.task +def update_all_user_kucoin_account(): + """fetch all avilable users and run a task for each""" + + users = models.User.objects.all() + for user in users: + update_kucoin_account.delay(user.username) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..ff6440c --- /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", "config.settings.dev") + 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/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..f48c786 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,73 @@ +amqp==5.1.1 +anyio==3.6.1 +asgiref==3.5.2 +asttokens==2.0.5 +async-timeout==4.0.2 +attrs==21.4.0 +backcall==0.2.0 +billiard==3.6.4.0 +black==22.6.0 +celery==5.2.7 +certifi==2022.6.15 +cffi==1.15.1 +charset-normalizer==2.1.0 +click==8.1.3 +click-didyoumean==0.3.0 +click-plugins==1.1.1 +click-repl==0.2.0 +cryptography==37.0.4 +decorator==5.1.1 +Deprecated==1.2.13 +Django==4.0.6 +django-environ==0.9.0 +django-filter==22.1 +django-redis==5.2.0 +djangorestframework==3.13.1 +djangorestframework-simplejwt==5.2.0 +drf-spectacular==0.22.1 +drf-spectacular-sidecar==2022.7.1 +environ==1.0 +executing==0.8.3 +gunicorn==20.1.0 +h11==0.13.0 +idna==3.3 +inflection==0.5.1 +ipython==8.4.0 +jedi==0.18.1 +jsonschema==4.7.2 +kombu==5.2.4 +matplotlib-inline==0.1.3 +mypy-extensions==0.4.3 +packaging==21.3 +parso==0.8.3 +pathspec==0.9.0 +pexpect==4.8.0 +pickleshare==0.7.5 +platformdirs==2.5.2 +prompt-toolkit==3.0.30 +psycopg2-binary==2.9.3 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pycparser==2.21 +pydantic==1.9.1 +Pygments==2.12.0 +PyJWT==2.4.0 +pyparsing==3.0.9 +pyrsistent==0.18.1 +pytz==2022.1 +PyYAML==6.0 +redis==4.3.4 +requests==2.28.1 +six==1.16.0 +sniffio==1.2.0 +sqlparse==0.4.2 +stack-data==0.3.0 +starlette==0.19.1 +tomli==2.0.1 +traitlets==5.3.0 +typing_extensions==4.3.0 +uritemplate==4.1.1 +urllib3==1.26.10 +vine==5.0.0 +wcwidth==0.2.5 +wrapt==1.14.1 diff --git a/start-service.dev.sh b/start-service.dev.sh new file mode 100644 index 0000000..42cf2e1 --- /dev/null +++ b/start-service.dev.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +#Active all env vars for docker compose files +. ./active_dev.sh + +docker compose -f ./docker-compose.yaml -f docker-compose.dev.yaml up -d \ No newline at end of file diff --git a/third_party_sdks/__init__.py b/third_party_sdks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/third_party_sdks/kucoin.py b/third_party_sdks/kucoin.py new file mode 100644 index 0000000..41dbd3e --- /dev/null +++ b/third_party_sdks/kucoin.py @@ -0,0 +1,240 @@ +"""Kucoin SDK""" + +import base64 +import hashlib +import hmac +import json +import time +from urllib import response +from urllib.parse import urlencode, urljoin +from uuid import uuid4 + +import requests + +from third_party_sdks.sdk import SDK + + +class Kucoin(SDK): + key: str + secret: str + passphrase: str + signature: str + + def __init__(self, sand_box=False) -> None: + if sand_box: + self.base_url: str = "https://openapi-sandbox.kucoin.com/" + else: + self.base_url: str = "https://api.kucoin.com/" + + @property + def now_as_mili(self) -> int: + """Get time as miliseconds""" + + return int(time.time() * 1000) + + def set_key(self, key: str) -> None: + self.key = key + + def set_secret(self, secret: str) -> None: + self.secret = secret + + def set_passphrase(self, passphrase: str) -> None: + self.passphrase = base64.b64encode( + hmac.new( + self.secret.encode("utf-8"), + passphrase.encode("utf-8"), + hashlib.sha256, + ).digest() + ) + + def dict_to_json(self, data) -> str: + return json.dumps(data) + + def _generate_header(self) -> dict: + """Generating header for request, based on the instance data + + Returns: + dict: headers + """ + + headers: dict = { + "KC-API-KEY": self.key, + "KC-API-SIGN": self.signature, + "KC-API-TIMESTAMP": str(self.now_as_mili), + "KC-API-PASSPHRASE": self.passphrase, + "KC-API-KEY-VERSION": "2", + "Content-Type": "application/json", + } + return headers + + def _generate_signature(self, method: str, url: str, body="") -> None: + """Generate a new signature based on the method name and URL and time, + to confirm the request from Kucoin, + [RUN BEFORE generate_header MEHTOD] + + Args: + method (str) : method name like 'GET' + url (str) : path of the target endpoint like '/api/v1/accounts' + body (str) : body of request + """ + + url = "/" + url.strip().strip("/") # normalize url + str_to_sign = str(self.now_as_mili) + method.upper() + url + body + + self.signature = base64.b64encode( + hmac.new( + self.secret.encode("utf-8"), str_to_sign.encode("utf-8"), hashlib.sha256 + ).digest() + ) + + def post_query(self, url: str, **data: dict) -> requests.Response: + """Send POST query to the API + + Args: + url (str): path of endpoint + data (dict, optional): body of request. Defaults to {}. + + Returns: + requests.Response: result of query + """ + + method = "POST" + data = self.dict_to_json(data) + + self._generate_signature(method, url, data) + headers = self._generate_header() + + return self.raw_query( + method, + url, + headers=headers, + data=data, + ) + + def get_query(self, url: str, **parameters: dict) -> requests.Response: + """Send GET Query to the API + + Args: + url (str): path of endpoint + parameters (dict, optional): parameters of url. Defaults to {}. + + Returns: + requests.Response: result of query + """ + + method = "GET" + if parameters != {}: + # parameters should have the character "?" at the beginning of themself + data = "?" + urlencode(parameters) + else: + data = "" + + self._generate_signature(method, url, data) + + headers = self._generate_header() + return self.raw_query( + method, + url, + headers=headers, + parameters=parameters, + ) + + def raw_query( + self, + method: str, + url: str, + headers: dict = {}, + data: dict = {}, + parameters: dict = {}, + ) -> requests.Response: + """Sending a raw request to the endpoint based on the method name and URL path + + Args: + method (str): the method name : 'GET', 'POST' ... + url (str): the path of endpoint : 'api/v1/accounts' ... + header (dict, optional): headers of reqeust . Defaults to {}. + data (dict, optional): sending some pyload beside the URL with Post, Put methods . Defaults to {}. + parameters (dict, optional): sending some parameters beside the URL . Defaults to {}. + + Returns: + Respoinse + """ + + full_url = urljoin(self.base_url, url) + response = requests.request( + method, + full_url, + headers=headers, + data=data, + params=parameters, + ) + + return response + + def get_account(self, accountID: str = None): + """get all accounts or get a specified account with the id of that + + Args: + accountID (str, optional): ID of account. Defaults is None. + + Returns: + list : a list of accounts + dict : details of account + """ + + path = "/api/v1/accounts" + + if accountID: + path = path + "/" + accountID + print(path) + response = self.get_query(path) + result = response.json() + + else: + response = self.get_query(path) + result = response.json()["data"] + + return result + + def new_position(self, **kwargs: dict): + """apply for new positions + + Args : + side (str) : buy or sell + symbol (str) : a valid trading symbol code. e.g. ETH-BTC + clientOid (str) [Optional] : Unique order id created by users to identify their orders, e.g. UUID. + type (str) [Optional] : limit or market (default is limit) + remark (str) [Optional] : remark for the order, length cannot exceed 100 utf8 characters + stp (str) [Optional]: self trade prevention , CN, CO, CB or DC + tradeType (str) [Optional] : The type of trading : TRADE(Spot Trade) Default is TRADE. + + Addistional args based on type : + type : market + size (str) [Optional] : Desired amount in base currency + funds (str) [Optional] : The desired amount of quote currency to use + + type : limit + price (str) : price per base currency + size (str) : amount of base currency to buy or sell + timeInForce (str) [Optional] : GTC, GTT, IOC, or FOK (default is GTC), read Time In Force. + cancelAfter (int) [Optional] : cancel after n seconds, requires timeInForce to be GTT + postOnly (bool) [Optional] : Post only flag, invalid when timeInForce is IOC or FOK + hidden (bool) [Optional] : Order will not be displayed in the order book + iceberg (bool) [Optional] : Only aportion of the order is displayed in the order book + visibleSize (str [Optional] : The maximum visible size of an iceberg order + Raises: + ValueError: pass invalid args or not passed requierd data + """ + + if not kwargs: + raise ValueError( + "See this link for more info : https://docs.kucoin.com/#place-a-new-order" + ) + + kwargs.setdefault("clientOid", str(uuid4())) + path = "/api/v1/orders" + response = self.post_query(path, **kwargs) + print(response.json()) + + + diff --git a/third_party_sdks/sdk.py b/third_party_sdks/sdk.py new file mode 100644 index 0000000..462382e --- /dev/null +++ b/third_party_sdks/sdk.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractclassmethod + + +class SDK(ABC): + @abstractclassmethod + def get_query(): + ... + + @abstractclassmethod + def post_query(): + ... + + @abstractclassmethod + def new_position(self, **kwargs): + ... + + @abstractclassmethod + def get_account(self): + ... diff --git a/third_party_sdks/sdk_api.py b/third_party_sdks/sdk_api.py new file mode 100644 index 0000000..63a253c --- /dev/null +++ b/third_party_sdks/sdk_api.py @@ -0,0 +1,9 @@ +from third_party_sdks.kucoin import Kucoin + + +def fetch_accounts(key, secret, passphrase, id=None) -> dict: + kucoin = Kucoin(False) + kucoin.set_key(key) + kucoin.set_secret(secret) + kucoin.set_passphrase(passphrase) + return kucoin.get_account(id)