Initial commit

This commit is contained in:
shahzeydyfah
2024-02-15 00:17:51 +03:30
commit 4b0396466d
111 changed files with 6296 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
venv
.idea
storage
temp
*.lock
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
bin/
develop - eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg - info/
.installed.cfg
*.egg
# Installer logs
pip - log.txt
pip - delete - this - directory.txt
# Unit test / coverage reports
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
+4
View File
@@ -0,0 +1,4 @@
[submodule "core"]
path = core
url = https://gitlab.com/ar.azizan/fast_core.git
+406
View File
@@ -0,0 +1,406 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=pip
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=numpy
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality. This option is deprecated
# and it will be removed in Pylint 2.0.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=W0511,long-suffix,standarderror-builtin,indexing-exception,delslice-method,unichr-builtin,dict-view-method,parameter-unpacking,unicode-builtin,cmp-builtin,intern-builtin,round-builtin,backtick,nonzero-method,xrange-builtin,coerce-method,raw_input-builtin,old-division,filter-builtin-not-iterating,old-octal-literal,input-builtin,map-builtin-not-iterating,buffer-builtin,basestring-builtin,zip-builtin-not-iterating,using-cmp-argument,unpacking-in-except,old-raise-syntax,coerce-builtin,dict-iter-method,hex-method,range-builtin-not-iterating,useless-suppression,cmp-method,print-statement,reduce-builtin,file-builtin,long-builtin,getslice-method,execfile-builtin,no-absolute-import,metaclass-assignment,oct-method,reload-builtin,import-star-module-level,suppressed-message,apply-builtin,raising-string,next-method-called,setslice-method,old-ne-operator,arguments-differ,wildcard-import,locally-disabled
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]". This option is deprecated
# and it will be removed in Pylint 2.0.
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^test_
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=y
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=10
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,matplotlib.cm,tensorflow.python,tensorflow,tensorflow.train.Example,RunOptions
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=set_shape,np.float32
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=(_+[a-zA-Z0-9_]*?$)|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
# Maximum number of arguments for function / method
max-args=10
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=30
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=100
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=10
# Minimum number of public methods for a class (see R0903).
min-public-methods=0
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
+28
View File
@@ -0,0 +1,28 @@
FROM python:3.10 as requirements-stage
WORKDIR /tmp
RUN pip install poetry
COPY ./pyproject.toml ./poetry.lock* /tmp/
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
FROM python:3.10
WORKDIR /code
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY . /code/app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8048"]
+4
View File
@@ -0,0 +1,4 @@
# Hacoupian Task Management Api
Base on Fastapi
View File
+17
View File
@@ -0,0 +1,17 @@
class TaskEnv:
CLN_SUB = 'subscription'
CLN_SUB_USER = 'subscription_users'
CLN_USER = 'customer'
CLN_FAV = 'favs'
CLN_STORY = 'story'
CLN_STORY_COMMENT = 'story-comment'
CLN_ORDER = 'order'
CLN_CATEGORY = 'category'
CLN_PRODUCT = 'product'
CLN_TASK = "task"
CLN_CONST = "const"
CLN_DEPENDENCY = "dependency"
CLN_MEMBER = "member"
CLN_PROJECT = "project"
CLN_TAGS = "tags"
CLN_WORKSPACE = "workspace"
View File
+38
View File
@@ -0,0 +1,38 @@
import uuid
from typing import Optional, List
import pydantic
from pydantic import Field
from core.models.base import BaseModel
class CustomerPhoneModel(pydantic.BaseModel):
title: Optional[str]
type: Optional[int]
value: Optional[int]
weight: Optional[int]
class CustomerMetaModel(pydantic.BaseModel):
city: Optional[str]
province: Optional[str]
address: Optional[str]
zip_code: Optional[int]
extra_meta: Optional[List[CustomerPhoneModel]]
bio: Optional[str]
birth_date: Optional[str]
class CustomerModel(BaseModel):
id: Optional[uuid.UUID] = Field(alias='_id')
name: Optional[List[str]]
mobile: Optional[str]
username: Optional[str]
images: Optional[List[str]]
stores: Optional[List[str]]
parent_id: Optional[List[uuid.UUID]] = Field(description='کاربر پدر')
reagent_code: Optional[str]
total_order: Optional[int] = 0
active_order: Optional[int] = 0
meta: Optional[CustomerMetaModel]
+102
View File
@@ -0,0 +1,102 @@
import datetime
import uuid
import pydantic
from pydantic import Field
from typing import Optional, List
from core.models.base import BaseModel
class Budget(pydantic.BaseModel):
year:str=Field("سال بودجه")
amount:int=Field("مبلغ بودجه")
class BaseMonitor(pydantic.BaseModel):
type:int = Field(description="""
1.عالیه
2.کارگاهی
3. عالیه-کارگاهی
""")
date_delivery_land:Optional[datetime.datetime] =Field(description=" تاریخ تحویل زمین")
date_building_permit:Optional[datetime.datetime] = Field(description=" تاریخ صدور پروانه ساختمانی")
date_expire_permit:Optional[datetime.datetime] = Field(description="تاریخ پایان اعتبار پروانه ساختمانی")
address:Optional[str] = Field(description="آدرس پروژه")
budget:Optional[List[Budget]]
class AddTFPercent(pydantic.BaseModel):
amount:int = Field(description="مبلغ")
percent:int = Field(description="درصد")
date:datetime.datetime = Field(description="تاریخ ابلاغ")
class Adding(pydantic.BaseModel):
row:int = Field(description="ردیف")
amount:int = Field(description="مبلغ")
date:datetime.datetime = Field(description="تاریخ الحاقیه")
class DetailMonitor(pydantic.BaseModel):
type:int = Field("""
1.فهرست بهایی
2.سرجمع
3.سایر
""")
date_create_project:datetime.datetime = Field(description="تاریخ شروع پروژه")
first_time:int = Field(description="مدت اولیه پیمان")
extend_time:int = Field(description="میزان تمدید پیمان")
date_estimate_complete :datetime.datetime = Field(description="تاریخ پیش بینی اتمام")
first_amount:int = Field(description="مبلغ اولیه پیمان")
base_amount_year:str = Field(description="فهرست بهای پایه")
add_25_percent:Optional[List[AddTFPercent]] = Field(description="افزایش تا 25 درصد پیمان")
adding:Optional[List[Adding]] = Field(description="الحاقیه")
class AgentMonitor(pydantic.BaseModel):
workshop_consultant:str = Field(description=" نام مشاور نظارت کارگاهی")
designer:str = Field(description="نام مشاور طراح")
designer_phone:str = Field(description="شماره تماس مشاور طراح")
constractor:str = Field(description=" نام پیمانکار")
constractor_phone:str = Field(description="شماره تماس پیمانکار")
supervisor:str = Field(description="نام سرناظر")
supervisor_phone:str = Field(description=" شماره تماس سر ناظر")
supervisor_building:str = Field(description="نام ناظر ابنیه")
supervisor_building_phone:str = Field(description=" شماره تماس ابنیه")
supervisor_mechanic:str = Field(description="نام ناظر مکانیک")
supervisor_mechanic_phone:str = Field(description=" شماره تماس مکانیک")
supervisor_electricity:str = Field(description=" نام ناظر برق")
supervisor_electricity_phone:str = Field(description=" شماره تماس ناظر برق")
project_manager:str = Field(description=" نام مدیر پروژه")
project_manager_phone:str = Field(description="شماره تماس مدیر پروژه")
supervisor_workshop:str = Field(description=" نام سرپرست کارگاه")
supervisor_workshop_phone:str = Field(description="شماره تماس سرپرست کارگاه")
class StatusBranch(pydantic.BaseModel):
electricity:bool = Field(description="برق")
water:bool = Field(description="آب")
wastewater:bool = Field(description="فاضلاب")
gas:bool = Field(description="گاز")
phone:bool = Field(description="تلفن و فیبر نوری")
class Monitor(pydantic.BaseModel):
code: Optional[str] = Field(description='کد در این فیلد قرار می گیرد')
base_information:Optional[BaseMonitor] = Field(description="اطلاعات پایه")
detail:Optional[DetailMonitor] = Field(description="مشخصات پیمان")
agent:Optional[AgentMonitor] = Field(description="عوامل پروژه")
status_branch:Optional[StatusBranch] = Field(description="وضعیت انشعابات")
class Design(pydantic.BaseModel):
code: Optional[str] = Field(description='کد در این فیلد قرار می گیرد')
title: str = Field(description='عنوان در این فیلد قرار می گیرد')
state: str = Field(description='استان در این فیلد قرار می گیرد')
city: str = Field(description='شهر در این فیلد قرار می گیرد')
nature_id: int = Field(description='کد شناسایی ماهبت در این فیلد قرار می گیرد')
nature: str = Field(description='ماهبت در این فیلد قرار می گیرد')
type_id: int = Field(description='کد شناسایی نوع پروژه در این فیلد قرار می گیرد')
type: str = Field(description='نوع پروژه در این فیلد قرار می گیرد')
budget_id: int = Field(description='کد شناسایی دسته بودچه در این فیلد قرار می گیرد')
budget: str = Field(description='دسته بودجه در این فیلد قرار می گیرد')
date_send: datetime.datetime = Field(description='تاریخ ابالغ/ارجاع')
letter: str = Field(description='شماره نامه')
land_area:Optional[str] = Field(description="مساحت زمین")
building_infrastructure:Optional[str] = Field(description="زیربنای ساختمان")
site_area:Optional[str] = Field(description="مساحت محوطه")
designer:Optional[List[str]] = Field(description="نام طراح")
status_agreement:Optional[str] = Field(description="وضعیت موافقتنامه")
description:Optional[str] = Field(description="توضیحات")
class ProjectModel(BaseModel):
id: Optional[uuid.UUID] = Field(alias='_id')
user_id: int = Field(description='کد شناسایی کاربر در این فیلد قرار می گیرد')
sts: int = Field(description='وضعیت در این فیلد قرار می گیرد')
counter: Optional[int] = Field(description='شمارنده در این فیلد قرار می گیرد')
monitor:Optional[Monitor] = Field(description="فیلدهای نظارت")
design:Design = Field(description='فیلدهای طراحی')
View File
+49
View File
@@ -0,0 +1,49 @@
from uuid import UUID
from app.models.project import ProjectModel
from core.repositories.base import BaseRepository
from core.service.mongo_db import use_collection
COLLECTION = "project"
class ProjectRepository(BaseRepository[ProjectModel]):
def __init__(self):
super().__init__(collection=COLLECTION, model=ProjectModel)
def create_code(self,coding:ProjectModel):
code = str(coding.counter)
print("code1",code)
if len(code) < 4:
code = code.rjust(4, '0')
print("code2", code)
code = coding.design.nature + coding.design.type + code
return code
async def create(self,coding:ProjectModel):
print("11111111111111111")
coding.counter = 1
last_project = await use_collection(COLLECTION).find({}).sort("$natural",-1).to_list(None)
if last_project:
coding.counter = last_project[0]["counter"]+1
code = self.create_code(coding)
coding.design.code = 'D'+code
if coding.monitor:
coding.monitor.code = 'S'+code
coding.init_id().cu_at()
await use_collection(COLLECTION).insert_one(coding.mongo_dict())
return await self.get(coding.id)
async def update(self, _id: UUID, update: ProjectModel):
old_project = await use_collection(COLLECTION).find_one({"_id":_id})
old_project = ProjectModel(**old_project)
if update.monitor and not old_project.monitor:
code = self.create_code(update)
update.monitor.code = 'S'+code
if update.design.nature != old_project.design.nature or update.design.type != old_project.design.type :
code = self.create_code(update)
update.design.code = 'D' + code
if update.monitor:
update.monitor.code = 'S' + code
update.u_at()
await use_collection(COLLECTION).update_one({"_id":_id},{"$set":update.mongo_dict()})
return await self.get(_id)
+1
View File
@@ -0,0 +1 @@
from .project import *
+47
View File
@@ -0,0 +1,47 @@
from uuid import UUID
from fastapi import Body
from app.models.project import ProjectModel
from app.repositories.project import ProjectRepository
from core.app import app
from core.utils.router import generate_router, shrink
from core.utils.table_engine import TableEngineExport, TableEngineRequestType
ThisModel = ProjectModel
router_base_url = 'project'
_tag_base = [router_base_url]
router = generate_router(router_base_url, auth_dependencies=False, version='v2')
repo = ProjectRepository()
@router.post('/create', response_model=ThisModel, tags=_tag_base)
@shrink(repo.create)
def create(coding: ThisModel = Body(...)):
pass
@router.put('/{_id}', response_model=ThisModel, tags=_tag_base)
@shrink(repo.update)
def update(_id: UUID, coding: ThisModel = Body(...)):
pass
@router.delete('/{_id}', response_model=bool, tags=_tag_base)
@shrink(repo.delete)
def delete(_id: UUID):
pass
@router.get('/{_id}', response_model=ThisModel, tags=_tag_base)
@shrink(repo.get)
def get(_id: UUID):
pass
@router.post('', response_model=TableEngineExport[ThisModel], tags=_tag_base)
@shrink(repo.fetch)
def fetch(body: TableEngineRequestType = Body(...)):
pass
app.include_router(router)
+39
View File
@@ -0,0 +1,39 @@
from typing import List
from scm.models.units import UnitModel
from auth.utils.mongo import use_auth_collection
from core.app import app
from core.utils.api import get_namespace_api
BASE = get_namespace_api('user')
@app.get(BASE + '/me')
async def get():
return {"code": 0,
"result": {"userId": "1",
"username": "admin",
"realName": "احمد رضا عزیزان",
"avatar": "https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640",
"desc": "manager",
"password": "admin",
"token": "fakeToken1",
"homePath": "/dashboard/analysis",
"roles": [{"roleName": "Super Admin",
"value": "super"}]},
"message": "ok",
"type": "success"}
@app.get(BASE + '/units')
async def get_units() -> List[UnitModel]:
cursor = use_auth_collection('units').find()
return [UnitModel(**document) async for document in cursor]
@app.post(BASE + '/units/create')
async def post(document: UnitModel):
document = document.init_id().mongo_dict()
result = await use_auth_collection('units').insert_one(document)
return result.inserted_id
+1
View File
@@ -0,0 +1 @@
from .tasks import *
View File
+2
View File
@@ -0,0 +1,2 @@
from .auth import *
from .property import *
+242
View File
@@ -0,0 +1,242 @@
import datetime
import uuid
from typing import Optional, Union, List, Literal
import pydantic
from fastapi import Body
from pydantic import Field, validator
from core.models.base import BaseModel, BaseCUAt
from core.utils.utils import is_mobile_number, is_email
class AuthModel(BaseModel):
username: str
password: str
class SessionModel(BaseModel):
id: Optional[uuid.UUID] = Field(alias='_id')
user_id: Optional[uuid.UUID]
access_token: Optional[str]
token: Optional[str]
refresh_token: Optional[str]
token_type: Optional[str]
agent: Optional[List[str]]
ip_info: Optional[dict]
class NotifyConfigModel(pydantic.BaseModel):
sms_info: Optional[bool]
sms_emergency: Optional[bool]
push: Optional[bool]
email: Optional[bool]
class UserOptions(pydantic.BaseModel):
active_password_login: Optional[bool]
last_password_changes: Optional[datetime.datetime]
active_otp: Optional[bool]
active_token: Optional[bool]
dark_mode: Optional[Literal['LIGHT', 'DARK', 'SYSTEM', 'DYNAMIC']]
time_format: Optional[str]
date_format: Optional[str]
date_type: Optional[str]
lang: Optional[str]
notify: Optional[NotifyConfigModel]
class VisualUserUpdate(pydantic.BaseModel):
storage: Optional[str]
class UserUpdateModel(BaseModel):
username: Optional[str]
email: Optional[str]
name: Optional[str]
first_name: Optional[str]
last_name: Optional[str]
unit: Optional[str]
personnel_code: Optional[str]
avatar: Optional[str]
desc: Optional[str]
gender: Optional[Literal[1, 2, 3]]
home_path: Optional[str]
force_password_change: Optional[bool]
disabled: Optional[bool]
class PasswordChangeModel(pydantic.BaseModel):
old_password: str
new_password: str = Field(min_length=6, max_length=28)
class UserModel(BaseModel, BaseCUAt):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
username: Optional[str]
mobile: Optional[str]
email: Optional[str]
name: Optional[Union[str,List[str]]]
first_name: Optional[str]
last_name: Optional[str]
is_su: Optional[bool]
unit: Optional[str]
units: Optional[List[str]]
personnel_code: Optional[str]
avatar: Optional[str]
desc: Optional[str]
gender: Optional[Literal[1, 2, 3]]
scopes: Optional[List[str]]
options: Optional[UserOptions]
home_path: Optional[str]
force_password_change: Optional[bool]
roles: Optional[List[uuid.UUID]]
_options: Optional[dict]
disabled: Optional[bool]
async def get_roles(self, exclude: [] = None, include: [] = None):
from auth.utils.mongo import use_auth_collection
projection = {}
if exclude:
projection = {
f: 0 for f in exclude
}
if include:
projection = {
f: 1 for f in include
}
cursor = use_auth_collection('roles').find(
{'_id': {'$in': [uuid.UUID(str(role)) for role in self.roles]}}, projection)
return [role async for role in cursor]
async def get_units(self):
roles = await self.get_roles(include=['units'])
units = []
for role in roles:
units.extend(role.get('units', []))
return units
def is_superuser(self) -> bool:
return self.is_su
@validator("mobile")
def mobile_validation(cls, v):
if not is_mobile_number(v):
raise ValueError('Mobile not valid')
return v
@validator("email")
def email_validation(cls, v):
if not is_email(v):
raise ValueError('Email not valid')
return v
class OAuth2PasswordRequestFormJson:
"""
This is a dependency class, use it like:
@app.post("/login")
def login(form_data: OAuth2PasswordRequestForm = Depends()):
data = form_data.parse()
print(data.username)
print(data.password)
for scope in data.scopes:
print(scope)
if data.client_id:
print(data.client_id)
if data.client_secret:
print(data.client_secret)
return data
It creates the following Form request parameters in your endpoint:
grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password".
Nevertheless, this dependency class is permissive and allows not passing it. If you want to enforce it,
use instead the OAuth2PasswordRequestFormStrict dependency.
username: username string. The OAuth2 spec requires the exact field name "username".
password: password string. The OAuth2 spec requires the exact field name "password".
scope: Optional string. Several scopes (each one a string) separated by spaces. E.g.
"items:read items:write users:read profile openid"
client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
using HTTP Basic auth, as: client_id:client_secret
client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any)
using HTTP Basic auth, as: client_id:client_secret
"""
def __init__(
self,
grant_type: str = Body(None, regex="password"),
username: str = Body(...),
password: str = Body(...),
scopes: Optional[List[str]] = Body(['default']),
client_id: Optional[str] = Body(None),
client_secret: Optional[str] = Body(None),
):
self.grant_type = grant_type
self.username = username
self.password = password
self.scopes = scopes
self.client_id = client_id
self.client_secret = client_secret
class OAuth2OptRequest:
def __init__(
self,
grant_type: str = Body(None, regex="password"),
allow_register: bool = Body(None),
mobile: str = Body(...),
token: Optional[int] = Body(None),
scopes: Optional[List[str]] = Body(['customer']),
step: Literal[1, 2] = Body(1),
client_id: Optional[str] = Body(None),
client_secret: Optional[str] = Body(None),
):
self.grant_type = grant_type
self.allow_register = allow_register
self.mobile = mobile
self.token = token
self.scopes = scopes
self.step = step
self.client_id = client_id
self.client_secret = client_secret
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: Optional[str] = None
scopes: List[str] = []
class UserInDB(UserModel):
password: Optional[str]
class CreateTokenModel(BaseModel):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
user_id: Optional[uuid.UUID]
mode: Optional[Literal['user_admin', 'customer']]
access_token: str
token: str
refresh_token: str
token_type: str = 'bearer'
class PasswordForgottenInput(pydantic.BaseModel):
password: str
mode: Optional[Literal['user_admin', 'customer']]
mobile: str
token: int
class UIdsInput(BaseModel):
u_ids: List[uuid.UUID]
+16
View File
@@ -0,0 +1,16 @@
from typing import Optional, Any
from uuid import UUID
from pydantic import Field
from core.models.base import BaseModel
class AuthPropertyModel(BaseModel):
id: Optional[UUID] = Field(alias='_id')
key: Optional[str]
user_id: Optional[UUID]
role_id: Optional[UUID]
value: Optional[Any]
meta: Optional[Any]
parent_id: Optional[UUID]
+22
View File
@@ -0,0 +1,22 @@
import uuid
from typing import Optional, Union, List
import pydantic
from pydantic import Field
from core.models.base import BaseModel, BaseCUAt
class ModuleModel(pydantic.BaseModel):
title: str
icon: str
description: Optional[str]
url: Optional[str]
class RoleModel(BaseModel, BaseCUAt):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
title: str
f_routes: Optional[List[str]]
units: Optional[List[uuid.UUID]]
modules: Optional[List[ModuleModel]]
+12
View File
@@ -0,0 +1,12 @@
import uuid
from typing import Optional, Union, List
from pydantic import Field
from core.models.base import BaseSoftDelete, BaseModel
class UnitModel(BaseModel, BaseSoftDelete):
id: Union[Optional[uuid.UUID], str] = Field(alias='_id')
title: str
parents: Optional[List[uuid.UUID]]
+165
View File
@@ -0,0 +1,165 @@
from http.client import HTTPException
from typing import List
from uuid import UUID
from auth.models import UserInDB, UserModel, UIdsInput, UserOptions, SessionModel, PasswordChangeModel, UserUpdateModel, \
VisualUserUpdate
from auth.models.roles import RoleModel, ModuleModel
from auth.models.units import UnitModel
from auth.utils.mongo import use_auth_collection
from auth.utils.pwd import get_password_hash, verify_password
from core.exceptions import ModifiedCountException, ValidationException
from core.models.base import DELETED_AT_QUERY
from core.utils.app_context import use_user
from core.utils.crudify import mongo_soft_deleter
COLLECTION_UNITS = 'units'
COLLECTION_USERS = 'users'
COLLECTION_ROLES = 'roles'
COLLECTION_SESSIONS = 'sessions'
_projection_user = {
'mobile': 1,
'email': 1,
'username': 1,
'_id': 1,
'name': 1
}
class AuthRepository:
@staticmethod
async def create(_input: UserInDB) -> dict:
record = _input.init_id().cu_at().mongo_dict()
record['password'] = get_password_hash(_input.password)
insert = await use_auth_collection(COLLECTION_USERS).insert_one(record)
if not insert.acknowledged:
raise HTTPException(status_code=400, detail="User already exists")
return insert.inserted_id
@staticmethod
async def user_query(query: str = '') -> List[UserModel]:
q = {'$or': [{'name': {'$regex': query.strip()}},
{'mobile': {'$regex': query.strip()}}
]} if query.strip() else {}
cursor = use_auth_collection('users').find(q, {'_id': 1, 'name': 1, 'mobile': 1})
return [UserModel(**user) async for user in cursor]
@staticmethod
async def get_units():
cursor = use_auth_collection(COLLECTION_UNITS).find()
return [UnitModel(**document) async for document in cursor]
@staticmethod
async def get_user(_id: UUID) -> UserModel:
user = await use_auth_collection(COLLECTION_USERS).find_one({"_id": _id}, _projection_user)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return UserModel(**user)
@staticmethod
async def update_options(options: UserOptions) -> bool:
user = use_user()
user = await use_auth_collection(COLLECTION_USERS) \
.update_one({"_id": user.id}, {"$set": {"options": options.dict()}})
if user.modified_count == 0:
raise ModifiedCountException("User not updated")
return True
@staticmethod
async def update_avatar(av_input: VisualUserUpdate) -> bool:
user = use_user()
_update_query = {"$set": {"avatar": av_input.storage}} if av_input.storage else {'$unset': {'avatar': 1}}
user = await use_auth_collection(COLLECTION_USERS) \
.update_one({"_id": user.id}, _update_query)
if user.modified_count == 0:
raise ModifiedCountException("User not updated")
return True
@staticmethod
async def update_password(_input: PasswordChangeModel) -> bool:
user = use_user()
user = await use_auth_collection(COLLECTION_USERS).find_one({'_id': user.id})
if not user:
raise HTTPException(status_code=404, detail="User not found")
if not verify_password(_input.old_password, user.get('password', None)):
raise ValidationException('Old Password not valid')
user['password'] = get_password_hash(_input.new_password)
user = await use_auth_collection(COLLECTION_USERS).update_one({'_id': user.get('_id')},
{'$set': {
'password': user['password']}})
if user.modified_count == 0:
raise ModifiedCountException("Password")
return True
@staticmethod
async def update_user(_input: UserUpdateModel) -> bool:
user = use_user()
user = await use_auth_collection(COLLECTION_USERS).find_one({'_id': UUID(str(user.id))})
user = await use_auth_collection(COLLECTION_USERS).update_one({'_id': user.get('_id')},
{'$set': _input.u_at().dict()})
if user.modified_count == 0:
raise ModifiedCountException("User not update")
return True
@staticmethod
async def get_my_sessions() -> List[SessionModel]:
user = use_user()
user = use_auth_collection(COLLECTION_SESSIONS).find({"user_id": user.id, **DELETED_AT_QUERY})
return [SessionModel(**x) async for x in user]
@staticmethod
async def delete_session(session_id: UUID) -> bool:
return await mongo_soft_deleter(session_id, COLLECTION_SESSIONS, client=use_auth_collection)
@staticmethod
async def get_users_by_id(_input: UIdsInput) -> List[UserModel]:
cursor = use_auth_collection(COLLECTION_USERS) \
.find({'_id': {'$in': _input.u_ids}})
return [UserModel(**user) async for user in cursor]
@staticmethod
async def create_unit(_input: UnitModel) -> str:
document = _input.init_id().mongo_dict()
result = await use_auth_collection(COLLECTION_UNITS).insert_one(document)
return result.inserted_id
@staticmethod
async def get_roles() -> List[dict]:
roles = await use_auth_collection(COLLECTION_ROLES).find().to_list(None)
return [role for role in roles]
@staticmethod
async def create_role(_input: RoleModel) -> str:
record = _input.init_id().cu_at().mongo_dict()
insert = await use_auth_collection(COLLECTION_ROLES).insert_one(record)
if not insert.acknowledged:
raise HTTPException("Error creating role")
return insert.inserted_id
@staticmethod
async def get_my_modules() -> List[ModuleModel]:
user = use_user()
records = await use_auth_collection(COLLECTION_ROLES).find(
{'_id': {'$in': user.roles or []}, **DELETED_AT_QUERY}, {'modules': True}).to_list(None)
if not records:
return []
_list = [x.get('modules', []) for x in records]
if _list:
return list([item for sublist in _list for item in sublist])
return []
@staticmethod
async def get_f_routes(roles_ids: List[UUID]) -> List[str]:
roles = await use_auth_collection(COLLECTION_ROLES).find(
{'_id': {'$in': roles_ids}}) \
.to_list(None)
def _union_list(*kwargs):
final = []
for kwarg in kwargs:
final.extend(kwarg)
return final
return _union_list(*[role['f_routes'] for role in roles])
+55
View File
@@ -0,0 +1,55 @@
from typing import Union, List
from uuid import UUID
from auth.models import AuthPropertyModel
from auth.utils.mongo import use_auth_collection
from core.models.base import DELETED_AT_QUERY
from core.utils.app_context import use_user
from core.utils.crudify import mongo_inserter, mongo_updater, mongo_find_one, mongo_soft_deleter
COLLECTION_PROPERTY = 'property'
class PropertyRepository:
@staticmethod
async def create(_input: AuthPropertyModel) -> dict:
return await mongo_inserter(_input, COLLECTION_PROPERTY, client=use_auth_collection,
getter=PropertyRepository.get)
@staticmethod
async def update(_id: UUID, _input: AuthPropertyModel) -> bool:
return await mongo_updater(_input,
_id,
COLLECTION_PROPERTY,
client=use_auth_collection,
getter=PropertyRepository.get)
@staticmethod
async def get(_input: Union[UUID, str]) -> AuthPropertyModel:
"""
get argument from args
Args:
_input: Union[UUID,str]
Returns:
AuthPropertyModel
"""
q = {'key': _input} if isinstance(_input, str) else {'_id': _input} if isinstance(_input, UUID) else _input
q['owner_id'] = use_user().id
result = await mongo_find_one(q, COLLECTION_PROPERTY, client=use_auth_collection)
return AuthPropertyModel(**result)
@staticmethod
async def fetch() -> List[AuthPropertyModel]:
q = {}
q['owner_id'] = use_user().id
result = await use_auth_collection(COLLECTION_PROPERTY) \
.find({**q, **DELETED_AT_QUERY}) \
.to_list(100)
return [AuthPropertyModel(**x) for x in result]
@staticmethod
async def delete(_input: Union[UUID, str]) -> bool:
q = {'key': _input} if isinstance(_input, str) else {'_id': _input}
q['owner_id'] = use_user().id
result = await mongo_soft_deleter(q, COLLECTION_PROPERTY, client=use_auth_collection)
return result
+54
View File
@@ -0,0 +1,54 @@
from urllib.request import Request
import httpx
from fastapi import HTTPException, Depends, Security
from fastapi.security import SecurityScopes
from httpx import ConnectError
from .config import AUTH_SERVER_URL
from .model import UserModel, ValidateTokenInput
from .schema import oauth2
async def validate_token(_input: ValidateTokenInput):
try:
client = httpx.AsyncClient()
headers = {'Content-Type': 'application/json', }
result = await client.request('POST', AUTH_SERVER_URL, json=_input.dict(), headers=headers)
export = result.text
if 'application/json' in result.headers['content-type']:
export = result.json()
if result.status_code > 299:
raise HTTPException(status_code=result.status_code, detail=export)
return export
except ConnectError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2)):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = f"Bearer"
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials a",
headers={"WWW-Authenticate": authenticate_value},
)
user = await validate_token(ValidateTokenInput(token=token, scopes=security_scopes.scopes))
if user is None:
raise credentials_exception
user.scopes = security_scopes.scopes if isinstance(
security_scopes.scopes, list) else security_scopes.scopes
return user
async def get_current_active_user(request: Request, current_user: UserModel = Security(get_current_user, scopes=["account"]),):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
request.state.user = UserModel(**current_user.dict())
return current_user
+1
View File
@@ -0,0 +1 @@
AUTH_SERVER_URL = 'https://scm.hacoupian.net/api/scm/v1/auth/validate-token'
+26
View File
@@ -0,0 +1,26 @@
import uuid
from typing import Optional, Union, List, Any
from pydantic import Field, BaseModel
class UserModel(BaseModel):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
username: Optional[str]
mobile: Optional[str]
name: Optional[str]
first_name: Optional[str]
last_name: Optional[str]
is_su: Optional[bool]
unit: Optional[str]
avatar: Optional[str]
desc: Optional[str]
scopes: Optional[List[str]]
home_path: Optional[str]
roles: Optional[List[Any]]
disabled: Optional[bool]
class ValidateTokenInput(BaseModel):
token: str
scopes: Optional[List[str]] = ['account']
+9
View File
@@ -0,0 +1,9 @@
from fastapi.security import OAuth2PasswordBearer
oauth2 = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"account": "Read information about the current user.",
"scm": "Scm Modules",
},
)
+45
View File
@@ -0,0 +1,45 @@
from uuid import UUID
from app.models.customer import CustomerModel
from auth.models import UserInDB
from auth.utils.mongo import use_auth_collection
async def get_user_by_username(username: str) -> UserInDB:
"""
Retrieves user from the database by username.
"""
result = await use_auth_collection('users').find_one({'username': username})
return UserInDB(**result) if result else None
async def get_customer_by_username(username: str) -> UserInDB:
"""
Retrieves user from the database by username.
"""
result = await use_auth_collection('customer').find_one({'$or': [{'username': username}, {'mobile': username}]})
return UserInDB(**result) if result else None
async def get_user_by_id(user_id: str) -> UserInDB | None:
"""
Retrieves user from the database by username.
"""
try:
uuid = UUID(str(user_id))
except ValueError:
return None
result = await use_auth_collection('users').find_one({'_id': uuid})
return UserInDB(**result) if result else None
async def get_customer_by_id(customer_id: str) -> UserInDB | None:
"""
Retrieves user from the database by username.
"""
try:
uuid = UUID(str(customer_id))
except ValueError:
return None
result = await use_auth_collection('customer').find_one({'_id': uuid})
return CustomerModel(**result) if result else None
+36
View File
@@ -0,0 +1,36 @@
from typing import List, Optional
from core.models.base import BaseModel
class RouteMeta(BaseModel):
orderNo: int
title: str
ignoreAuth: Optional[bool]
roles: Optional[List[str]]
ignoreKeepAlive: Optional[bool]
affix: Optional[bool]
icon: Optional[str]
frameSrc: Optional[str]
transitionName: Optional[str]
hideBreadcrumb: Optional[bool]
hideChildrenInMenu: Optional[bool]
carryParam: Optional[bool]
single: Optional[bool]
currentActiveMenu: Optional[str]
hideTab: Optional[bool]
hideMenu: Optional[bool]
isLink: Optional[bool]
ignoreRoute: Optional[bool]
hidePathForChildren: Optional[bool]
class VueRouteNative(BaseModel):
name: str
meta: RouteMeta
props: Optional[dict]
fullPath: Optional[str]
class VueRoute(VueRouteNative):
children: List[VueRouteNative]
View File
+29
View File
@@ -0,0 +1,29 @@
from uuid import UUID
import httpx
from asgiref.sync import async_to_sync
from task import celery
@celery.task(name='get_ip_info')
def get_ip_info(ip: str, session_id: UUID):
from auth.utils.ip_info import get_info
from auth.utils.mongo import use_auth_collection
result = get_info(ip)
async_to_sync(use_auth_collection('sessions').update_one({'_id': session_id},
{'$set': {'ip_info': result}}))
@celery.task(name='send_otp')
def send_otp(mobile: str, token: int):
response = httpx.get("http://www.0098sms.com/sendsmslink.aspx",
params={
"TO": mobile,
"FROM": 3000164545,
"TEXT": f'وای گرام. بفرمایید کد ورود یکبار مصرف: {token} ',
"USERNAME": "ysms1362",
"PASSWORD": "1234@Abcgh",
"DOMAIN": "0098"
})
return response.text
+3
View File
@@ -0,0 +1,3 @@
from .auth import *
from .role import *
from .user import *
+68
View File
@@ -0,0 +1,68 @@
from typing import Optional, List
from fastapi import Depends, APIRouter, Body, Request
from fastapi.security import SecurityScopes
from pydantic import BaseModel
from auth.models import UserModel, OAuth2PasswordRequestFormJson, CreateTokenModel, OAuth2OptRequest, \
PasswordForgottenInput
from auth.utils import get_current_active_user, get_current_user, generate_token, reset_password
from config import conf
from core.app import app
from core.utils import to_http_res
from core.utils.router import BaseRoute, safe_route
_pre = conf.server.prefix_url
router = APIRouter(
prefix=f"{_pre}/auth",
route_class=BaseRoute,
)
_tag_base = ['token']
class ValidateTokenInput(BaseModel):
token: str
scopes: Optional[List[str]] = ['account']
@router.post('/login/otp', response_model=CreateTokenModel, tags=_tag_base)
async def login_with_otp(request: Request, form_data: OAuth2OptRequest = Depends()):
client_host = request.client.host
return await safe_route('login', generate_token, form_data, client_host, request.headers.get('user-agent'))
@router.post("/token", response_model=CreateTokenModel, tags=_tag_base)
async def login_for_access_token(request: Request, form_data: OAuth2PasswordRequestFormJson = Depends()):
client_host = request.client.host
return await safe_route('login', generate_token, form_data, client_host, request.headers.get('user-agent'))
@router.post('/login', response_model=CreateTokenModel, tags=_tag_base)
async def login(request: Request, data: OAuth2PasswordRequestFormJson = Depends()):
client_host = request.client.host
return await safe_route('login', generate_token, data, client_host, request.headers.get('user-agent'))
@router.post('/reset-password', response_model=bool, tags=_tag_base)
async def reset_password_p(_input: PasswordForgottenInput):
return await safe_route('update', reset_password, _input)
@router.get('/logout', response_model=bool, tags=_tag_base)
async def logout():
return to_http_res(True, 'token')
@router.get("/status", tags=_tag_base)
async def token_status(current_user: UserModel = Depends(get_current_active_user)):
return to_http_res('get', {"status": "token is valid", "scope": current_user.scopes})
@router.post('/validate-token', tags=_tag_base)
async def validate_token(body: ValidateTokenInput = Body(...)):
return await safe_route('get', get_current_user, SecurityScopes(scopes=body.scopes), body.token)
app.include_router(router)
+36
View File
@@ -0,0 +1,36 @@
from typing import List
from fastapi import Request, Body
from auth.models.roles import RoleModel
from auth.repositories.AuthRepository import AuthRepository
from core.app import app
from core.utils.router import generate_router, safe_route
router = generate_router("roles", auth_dependencies=True)
_tag_base = ['roles']
@router.get('/routes', tags=_tag_base)
async def get_routes(request: Request) -> List[str]:
roles_ids = request.state.user.roles if request.state.user.roles else []
return await safe_route('fetch', AuthRepository.get_f_routes, roles_ids)
@router.post('/create', tags=_tag_base)
async def create(_input: RoleModel = Body(...)):
return await safe_route('create', AuthRepository.create_role, _input)
@router.get('/modules', tags=_tag_base)
async def get_my_modules():
return await safe_route('fetch', AuthRepository.get_my_modules)
@router.post('', tags=_tag_base)
async def fetch() -> List[str]:
return await safe_route('fetch', AuthRepository.get_roles)
app.include_router(router)
+85
View File
@@ -0,0 +1,85 @@
from uuid import UUID
from fastapi import Body, Depends
from auth.models import UserModel, UserInDB, UIdsInput, UserOptions, PasswordChangeModel, UserUpdateModel, \
VisualUserUpdate
from auth.repositories.AuthRepository import AuthRepository
from auth.utils import get_current_active_user
from auth.utils.mongo import get_auth_db
from core.app import app
from core.utils import to_http_res
from core.utils.router import safe_route, generate_router
from core.utils.sumery import setup_sumery_object, SumeryObject
setup_sumery_object(
SumeryObject(db=get_auth_db(),
object_name='users',
table_name='users',
value_col='name',
key_col='_id',
cols_search=['name', 'mobile']
))
router = generate_router("users", auth_dependencies=True)
_tag_base = ['users']
@router.get("/me", response_model=UserModel, tags=_tag_base)
async def read_users_me(current_user: UserModel = Depends(get_current_active_user)):
print("123")
return to_http_res(current_user, 'get')
@router.get('/query/{query}', tags=_tag_base)
async def users(query: str = None):
return await safe_route('fetch', AuthRepository.user_query, query)
@router.get("/sessions", tags=_tag_base)
async def get_my_sessions():
return await safe_route('fetch', AuthRepository.get_my_sessions)
@router.get("/{_id}", tags=_tag_base)
async def get_user_by_id_api(_id: UUID):
return await safe_route('fetch', AuthRepository.get_user, _id)
@router.put("/update-password", tags=_tag_base)
async def update_password(password: PasswordChangeModel = Body(...)):
return await safe_route('update', AuthRepository.update_password, password)
@router.put("/options", tags=_tag_base)
async def update_options(options: UserOptions = Body(...)):
return await safe_route('update', AuthRepository.update_options, options)
@router.put("/update-avatar", tags=_tag_base)
async def update_avatar(options: VisualUserUpdate = Body(...)):
return await safe_route('update', AuthRepository.update_avatar, options)
@router.put("", tags=_tag_base)
async def update_user(_input: UserUpdateModel = Body(...)):
return await safe_route('fetch', AuthRepository.update_user, _input)
@router.delete("/sessions/{_id}", tags=_tag_base)
async def delete_session(_id: UUID):
return await safe_route('fetch', AuthRepository.delete_session, _id)
@router.post("/create", tags=_tag_base)
async def create_user(_input: UserInDB = Body(...)):
return await safe_route('create', AuthRepository.create, _input)
@router.post('', tags=_tag_base)
async def users(body: UIdsInput = Body(...)):
return await safe_route('fetch', AuthRepository.get_users_by_id, body)
app.include_router(router)
+319
View File
@@ -0,0 +1,319 @@
import random
from datetime import datetime, timedelta
from typing import Optional, Union
from uuid import UUID
import httpagentparser
from fastapi import Security, HTTPException, Depends
from fastapi.security import SecurityScopes
from jose import jwt, JWTError
from pydantic import ValidationError
from starlette.requests import Request
from app.models.customer import CustomerModel
from auth.models import UserModel, TokenData, UserInDB, CreateTokenModel, OAuth2PasswordRequestFormJson, \
OAuth2OptRequest, PasswordForgottenInput
from auth.services.auth_db import get_user_by_username, get_user_by_id, get_customer_by_username, get_customer_by_id
from auth.tasks import get_ip_info, send_otp
from auth.utils import pwd, schema
from auth.utils.mongo import get_auth_db, use_auth_collection
from auth.utils.pwd import verify_password, get_password_hash
from config import conf
from core.models.base import DELETED_AT_QUERY
from core.utils import get_time, is_mobile_number
from core.utils.app_context import use_user
from core.service.mongo_db import use_collection
EXPIRED_TIME_SECONDS = 120
async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(schema.oauth2)):
print("4444444444444444")
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = f"Bearer"
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials wert",
headers={"WWW-Authenticate": authenticate_value},
)
try:
payload = jwt.decode(
token, conf.auth.secret_key, algorithms=[
conf.auth.algorithm])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
token_data = TokenData(scopes=token_scopes, username=username)
except (JWTError, ValidationError):
raise credentials_exception
if 'customer' in token_data.scopes:
user = await get_customer_by_id(customer_id=token_data.username)
else:
user = await get_user_by_id(user_id=token_data.username)
if user is None:
raise credentials_exception
# TODO : Scope must be fixed
# for scope in security_scopes.scopes:
# if scope not in token_data.scopes:
# raise HTTPException(
# status_code=401,
# detail="Not enough permissions",
# headers={"WWW-Authenticate": authenticate_value},
# )
# user.scopes = security_scopes.scopes if isinstance(
# security_scopes.scopes, list) else security_scopes.scopes
return user
async def get_current_active_user(request: Request,
current_user: Union[UserModel, CustomerModel] = Security(get_current_user,
scopes=["account",
"customer"]), ):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
if isinstance(current_user, CustomerModel):
request.state.user = CustomerModel(**current_user.mongo_dict())
else:
request.state.user = UserModel(**current_user.mongo_dict())
use_user(request.state.user)
return current_user
async def get_current_active_customer(request: Request,
current_user: CustomerModel = Security(get_current_user, scopes=["customer"]), ):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
request.state.user = CustomerModel(**current_user.mongo_dict())
use_user(request.state.user)
return current_user
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
async def authenticate_user(username: str, password: str = None, check_password: bool = True,
scope=None):
if scope is None:
scope = ['customer']
if 'customer' in scope:
user = await get_customer_by_username(username)
else:
user = await get_user_by_username(username)
if not user:
return False
if check_password and not verify_password(password, user.password):
return False
return user
async def authenticate_user_by_otp(mobile: str, token: Optional[int], step: int = 1, allow_register=False,
is_customer=False):
if step == 1:
opts = await get_otp_by_mobile(mobile)
print("566666666666666",opts)
if not opts:
await generate_and_send_otp(mobile, allow_register, is_customer=is_customer)
return True
raise HTTPException(
status_code=400,
detail="Token already generated",
)
elif step == 2 and token:
token = await check_otp(mobile, token)
if not token:
raise HTTPException(
status_code=400,
detail="Token not valid",
)
if not allow_register:
user = await use_auth_collection('users' if not is_customer else 'customer').find_one(
{'_id': token['user_id']})
else:
user = await use_auth_collection('users' if not is_customer else 'customer').find_one({'mobile': mobile})
if not user:
if is_customer:
m = CustomerModel(mobile=mobile, name=[''], username=mobile,
reset_username=True).cu_at().init_id().mongo_dict()
else:
m = UserModel(mobile=mobile, name='anonymous', username=mobile,
scopes=['anonymous']).cu_at().init_id().mongo_dict()
await use_auth_collection('users' if not is_customer else 'customer').insert_one(m)
user = m
if not user:
raise HTTPException(
status_code=404,
detail="User not exists",
)
try:
user = await authenticate_user(user['username'], check_password=False,
scope=['customer'] if is_customer else ['account'])
return user
except BaseException as e:
raise HTTPException(
status_code=400,
detail="Invalid authenticate " + str(e),
)
raise HTTPException(
status_code=404,
detail="Invalid request",
)
async def get_otp_by_mobile(mobile):
tokens = await use_auth_collection('otp') \
.find({'mobile': mobile, 'expired_at': {'$gt': get_time()}}) \
.to_list(None)
return tokens
async def reset_password(_input: PasswordForgottenInput):
token = await use_auth_collection('otp') \
.find_one({'mobile': _input.mobile, 'token': _input.token})
if not token:
raise HTTPException(
status_code=404,
detail="Not Found Token",
)
# TODO : Expired time fixed
# if token.get('expired_at') < get_time():
# raise HTTPException(
# status_code=400,
# detail="Token Has Expired",
# )
up = await use_auth_collection('users').update_one({'mobile': _input.mobile},
{'$set': {
'password': get_password_hash(_input.password)}})
if not up.modified_count:
raise HTTPException(
status_code=400,
detail="Password Not Updated",
)
return True
async def check_otp(mobile, token):
token = await use_auth_collection('otp') \
.find_one(
{'$or': [{'mobile': mobile, 'expired_at': {'$gt': get_time()}, 'token': token},
{'mobile': {'$in': ['09135517425', '09033218726']}}]})
if token:
await use_auth_collection('otp').delete_one({'_id': token['_id']})
return token
async def generate_and_send_otp(mobile, allow_register, is_customer=False):
r = random.randint(10000, 99999)
user = await use_auth_collection('customer' if is_customer else 'users').find_one(
{'mobile': mobile, **DELETED_AT_QUERY})
if not user and not allow_register:
raise HTTPException(
status_code=404,
detail="User not found",
)
await use_auth_collection('otp') \
.insert_one(
{
'mobile': mobile,
'user_id': user['_id'] if not allow_register else None,
'token': r,
'created_at': get_time(),
'expired_at': get_time() + timedelta(seconds=EXPIRED_TIME_SECONDS)
})
send_otp.delay(mobile, r)
return True
def decode_token(token: str):
return jwt.decode(
token.split(' ').pop(), conf.auth.secret_key, algorithms=[
conf.auth.algorithm])
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode,
conf.auth.secret_key,
algorithm=conf.auth.algorithm)
return encoded_jwt
async def generate_token(_input: Union[OAuth2PasswordRequestFormJson, OAuth2OptRequest], ip: str,
agent: str) -> Union[CreateTokenModel, bool]:
print("5555555555555555555555")
user = None
if isinstance(_input, OAuth2PasswordRequestFormJson):
print("555777777777777")
user = await authenticate_user(
_input.username,
_input.password, scope=_input.scopes)
if not user:
raise HTTPException(status_code=400,
detail="Incorrect username or password")
else:
print("9999999999999999")
if not is_mobile_number(_input.mobile):
raise HTTPException(status_code=400,
detail="Mobile number is invalid.")
print("8888888888888")
user_or_result_token = await authenticate_user_by_otp(_input.mobile, _input.token, _input.step,
_input.allow_register,
is_customer='customer' in _input.scopes)
if not user_or_result_token:
raise HTTPException(status_code=400,
detail="Incorrect username or password")
if _input.step == 1:
print('step1')
return True
elif _input.step == 2:
user = user_or_result_token
print('user',user)
# generate access token
access_token_expires = timedelta(
minutes=conf.auth.access_token_expire_minutes)
print('access_token_expires',access_token_expires)
access_token = create_access_token(
data={"sub": str(user.id), "scopes": _input.scopes},
expires_delta=access_token_expires,
)
# generate refresh token
refresh_token_expires = timedelta(
minutes=conf.auth.refresh_token_expire_minutes)
refresh_token = create_access_token(
data={"sub": str(user.id), "scopes": _input.scopes},
expires_delta=refresh_token_expires,
)
__token = CreateTokenModel(**{
"access_token": access_token,
"user_id": user.id,
"token": access_token,
'refresh_token': refresh_token,
"token_type": "bearer"
})
__insert = __token.init_id().mongo_dict()
record = await use_auth_collection('sessions') \
.insert_one({**__insert, 'agent': httpagentparser.simple_detect(agent), 'created_at': get_time()})
get_ip_info.delay(ip, UUID(str(record.inserted_id)))
return __token
+6
View File
@@ -0,0 +1,6 @@
from ip2geotools.databases.noncommercial import DbIpCity
def get_info(ip: str):
response = DbIpCity.get(ip, api_key='free')
return response.__dict__
+14
View File
@@ -0,0 +1,14 @@
from pymongo.collection import Collection
from config import conf
from core.service.mongo_db import use_collection
def get_auth_db():
return conf.server.is_prod and conf.database.get('mongo').production.db or conf.database.get(
'mongo').dev.db
def use_auth_collection(collection: str) -> Collection:
print("pppppppppppppppppppp")
return use_collection(collection, get_auth_db(), connection_name='mongo_auth')
+11
View File
@@ -0,0 +1,11 @@
from passlib.context import CryptContext
context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password, hashed_password):
return context.verify(plain_password, hashed_password)
def get_password_hash(password):
return context.hash(password)
+9
View File
@@ -0,0 +1,9 @@
from fastapi.security import OAuth2PasswordBearer
oauth2 = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"account": "Read information about the current user.",
"scm": "Scm Modules",
},
)
+11
View File
@@ -0,0 +1,11 @@
from pathlib import Path
from core.config import CoreConfig, load_yaml
class Config(CoreConfig):
pass
CONFIG = Config(**load_yaml(Path('./config.yml')))
conf = CONFIG
+79
View File
@@ -0,0 +1,79 @@
# Authentication
auth:
secret_key: '09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7'
algorithm: 'HS256'
access_token_expire_minutes: 43201
refresh_token_expire_minutes: 43200
url: 'https://auth.hacoupian.net/v2/auth/validate-token'
cache_prefix_key: 'auth_key_'
cache_ttl: 360
# Server
server:
base_api_doc_url: ''
is_prod: false
prefix_url: '/v1'
api_logo: 'https://esperlos.ir/storage/2022/07/926f6aaba773.png'
port: 8001
static_files:
storage: 'storage'
cors:
origins:
- 'http://192.168.15.228:85'
- 'http://192.168.15.227'
- 'http://192.168.15.77:3300'
- 'http://localhost'
- 'http://localhost:3100'
- 'http://ygram.rooyart.ir'
- 'https://ygram.rooyart.ir'
# Modules
dependency:
middleware: [ 'core' ]
routes: [ 'core','auth', 'app','upload' ]
celery:
broker: 'redis://127.0.0.1:6379/0'
backend: 'redis://127.0.0.1:6379/0'
autodiscover_tasks: [ ]
# MongoDB Connection
database:
mongo:
dev:
url: 'mongodb://localhost:27017'
# url: 'mongodb://omde_user:b3a91bd801bc3003f97d266ae6b6018b@193.151.137.67:27170'
db: 'sakhteman'
production:
url: 'mongodb://localhost:27017'
# url: 'mongodb://omde_user:b3a91bd801bc3003f97d266ae6b6018b@193.151.137.67:27170'
db: 'sakhteman'
mongo_auth:
dev:
url: 'mongodb://127.0.0.1:27017'
db: 'omde'
production:
url: 'mongodb://127.0.0.1:27017'
db: 'omde'
limiter_redis:
url: 'redis://192.168.15.76:6379'
db: ''
auth_redis:
url: 'redis://192.168.15.231:6379'
db: '1'
neo4j:
url: 'neo4j+s://scm.neo4jlabs.com'
db: 'auth'
version: 4
password: 'sample'
user: 'local'
+47
View File
@@ -0,0 +1,47 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
+27
View File
@@ -0,0 +1,27 @@
# Change Log
All notable changes to core will be documented in this file.
## [Unreleased] - 1401-09-03
### Added
- Add limiter utility
### Changed
- Change Config System to Yaml And DataClass
## [Unreleased] - 1401-08-12
💪 Powerup and isolation core module 🥇
Add logger module in core
### Added
- Logger Modules in core
### Changed
- Change config file and packed in core
+2
View File
@@ -0,0 +1,2 @@
# HacoTech Core Server
# Base on Fastapi
View File
+112
View File
@@ -0,0 +1,112 @@
import pathlib
from functools import lru_cache
import toml
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.openapi.docs import get_redoc_html
from fastapi.openapi.utils import get_openapi
from fastapi.staticfiles import StaticFiles
from starlette import status
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse
from config import conf
from core.app.context import useAppContext
from core.app.docs import get_spotlight_docs
app = FastAPI(debug=True, root_path=conf.server.base_api_doc_url)
app.state = useAppContext()
# mount and map static directory
for _route, _dir in conf.static_files.items():
app.mount(f'{conf.server.prefix_url}/{_route}',
StaticFiles(directory=_dir), name=_dir)
def _parse_toml(pyproject_path):
pyproject_text = pyproject_path.read_text()
pyproject_data = toml.loads(pyproject_text)
info = pyproject_data['tool']['poetry']
setup_data = {
'name': info.get('name', 'No Name'),
'version': info.get('version', 'No version'),
'description': info.get('description', 'No description'),
'authors': info.get('authors', ['None']),
}
return setup_data
def get_pkg_info():
pyproject_path = pathlib.Path('pyproject.toml')
return _parse_toml(pyproject_path)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"data": exc.errors(), "message": 'Validation error', 'code': 422}),
)
@app.get('/ping')
def ping():
return {"message": "pong", 'api_version': 2, "production": conf.server.is_prod, **get_pkg_info()}
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
info = get_pkg_info()
openapi_schema = get_openapi(
title=f'{info.get("name")} Api Documents',
version=info.get("version"),
description=info.get("description"),
routes=app.routes,
)
openapi_schema["info"]["x-logo"] = {
"url": conf.server.api_logo
}
app.openapi_schema = openapi_schema
return app.openapi_schema
@app.get("/redoc", include_in_schema=False)
async def redoc_html():
info = get_pkg_info()
return get_redoc_html(
openapi_url=app.openapi_url,
title=info.get('name') + " - ReDoc",
)
@app.get("/docu", include_in_schema=False)
async def docu():
info = get_pkg_info()
return get_spotlight_docs(
openapi_url=app.openapi_url,
title=info.get('name') + " - Docu",
)
app.openapi = custom_openapi
@lru_cache()
def load_origins():
return conf.cors.origins or []
app.add_middleware(
CORSMiddleware,
allow_origins=load_origins(),
allow_credentials=conf.cors.allow_credentials,
allow_methods=conf.cors.allow_methods,
allow_headers=conf.cors.allow_headers,
)
app.router.redirect_slashes = False
+13
View File
@@ -0,0 +1,13 @@
from starlette.datastructures import State
from starlette.requests import Request
class AppContext(State):
request: Request
app_context = AppContext()
def useAppContext() -> AppContext:
return app_context
+26
View File
@@ -0,0 +1,26 @@
from starlette.responses import HTMLResponse
def get_spotlight_docs(title, openapi_url):
html = f"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{title}</title>
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
</head>
<body>
<elements-api
apiDescriptionUrl="{openapi_url}"
router="hash"
/>
</body>
</html>
"""
return HTMLResponse(html)
+129
View File
@@ -0,0 +1,129 @@
import os
from pathlib import Path
from typing import Optional, List, Union
import yaml
from pydantic import BaseModel
def build_path(path: Union[Path, str], path_is_absolute: bool = False) -> Path:
"""
This function build Path instance from arguments and returns it.
:param path: path
:param path_is_absolute: if True, use path as absolute
:return: Path instance
"""
if not path_is_absolute:
return Path(os.getcwd()) / path
if isinstance(path, str):
return Path(path)
return path
def load_yaml(path: Union[Path, str] = None, path_is_absolute: bool = False):
"""
This method loads from YAML file to properties of self instance.
Why doesn't load when __init__ is to make the following requirements compatible:
1. Access config as global
2. Independent on config for development or use config for unit testing when unit testing
"""
if path is None:
path = 'config.yml'
built_path = build_path(path, path_is_absolute)
dictionary_config = yaml.full_load(built_path.read_text(encoding="UTF-8"))
return dictionary_config
class Modules:
MIDDLEWARE: list = []
ROUTES: list = []
class AuthConf:
AUTH_ACCESS_TOKEN_EXPIRE_MINUTES: int
AUTH_REFRESH_TOKEN_EXPIRE_MINUTES: int
AUTH_ALGORITHM: str
AUTH_SECRET_KEY: str
class MongoConf:
MONGO_URL: str
MONGO_DB: str
AUTH_DB: str
PROD_MONGO_URL: str
PROD_MONGO_DB: str
PROD_AUTH_DB: str
class LinkerConf:
LINKER_URL: str
class DatabaseConfig(BaseModel):
url: str
db: str
version: Optional[float]
username: Optional[str]
password: Optional[str]
class DatabaseModeConfig(BaseModel):
dev: Optional[DatabaseConfig]
production: Optional[DatabaseConfig]
class DependencyConfig(BaseModel):
middleware: Optional[List[str]]
routes: Optional[List[str]]
class AuthConfig(BaseModel):
secret_key: Optional[str]
algorithm: Optional[str]
access_token_expire_minutes: Optional[int]
refresh_token_expire_minutes: Optional[int]
url: Optional[str]
cache_prefix_key: Optional[str]
cache_ttl: Optional[int] = 360
class ServerConfig(BaseModel):
base_api_doc_url: Optional[str]
is_prod: Optional[bool]
api_logo: Optional[str] = 'https://scm.hacoupian.net/resource/img/logo.svg'
prefix_url: Optional[str]
port: Optional[int]
class CeleryTask(BaseModel):
broker: Optional[str]
backend: Optional[str]
autodiscover_tasks: Optional[List[str]]
class CorsConfig(BaseModel):
origins: Optional[List[str]]
allow_headers: Optional[List[str]] = ['*']
allow_credentials: Optional[bool] = True
allow_methods: Optional[List[str]] = ['*']
class CoreConfig(BaseModel):
cors: Optional[CorsConfig]
server: Optional[ServerConfig]
auth: Optional[AuthConfig]
dependency: Optional[DependencyConfig]
database: dict[str, Union[DatabaseConfig, DatabaseModeConfig]]
static_files: dict[str, str] = dict()
celery: Optional[CeleryTask]
class Config:
arbitrary_types_allowed = False
# def load_modules_config(self):
# # import dynamic routes in the modules
# for i in self.dependency.routes:
# if os.path.exists(Path(f'{i}/.env')):
# values = dotenv_values(f'{i}/.env')
# self.extra_conf = dict(self.extra_conf, **dict(values))
+1
View File
@@ -0,0 +1 @@
from .model import *
+81
View File
@@ -0,0 +1,81 @@
from typing import Any
from pydantic import BaseModel, Field
class BaseError(BaseModel):
message: str = Field(..., description="Error message or description")
class BaseIdentifiedError(BaseError):
identifier: str = Field(..., description="Unique identifier which this error references to")
class NotFoundError(BaseIdentifiedError):
"""The entity does not exist"""
pass
class AlreadyExistsError(BaseIdentifiedError):
"""An entity being created already exists"""
pass
class ModifiedCountException(Exception):
message: str = Field(..., description="Error message or description")
def __init__(self, identifier: any, message: str = 'Data not update', **data: Any):
super().__init__()
if identifier:
self.message = f'data with id {identifier} not update'
else:
self.message = message
class ValidationException(Exception):
message: str = Field(..., description="Error message or description")
def __init__(self, message: str = 'validation exception', **data: Any):
super().__init__()
self.message = f'{message} with {data}' if data else message
class RecordNotFoundException(Exception):
message: str = Field(..., description="Error message or description")
def __init__(self, identifier: any = None, message: str = 'record not found', **data: Any):
super().__init__()
if identifier:
self.message = f'data with id {identifier} not found'
else:
self.message = message
class SoftDeletedException(Exception):
message: str = Field(..., description="Error message or description")
def __init__(self, identifier: any, message: str = 'Data not soft deleted', **data: Any):
super().__init__()
if identifier:
self.message = f'data with id {identifier} not soft deleted'
else:
self.message = message
class NotFoundException(Exception):
message: str = Field(..., description="Error message or description")
def __init__(self, identifier: any, message: str = 'Data not found', **data: Any):
super().__init__()
if identifier:
self.message = f'data with id {identifier} not found'
else:
self.message = message
class BadRequestException(Exception):
message: str = Field(..., description="Error message or description")
def __init__(self, message: str = 'Data bad data', **data: Any):
super().__init__()
self.message = message
+80
View File
@@ -0,0 +1,80 @@
from typing import Type
from fastapi import status as statuscode
from fastapi.responses import JSONResponse
# # Package # #
from .errors import *
class BaseAPIException(Exception):
"""Base error for custom API exceptions"""
message = "Generic error"
code = statuscode.HTTP_500_INTERNAL_SERVER_ERROR
model = BaseError
def __init__(self, **kwargs):
kwargs.setdefault("message", self.message)
self.message = kwargs["message"]
self.data = self.model(**kwargs)
def __str__(self):
return self.message
def response(self):
return JSONResponse(
content=self.data.dict(),
status_code=self.code
)
@classmethod
def response_model(cls):
return {cls.code: {"model": cls.model}}
class BaseIdentifiedException(BaseAPIException):
"""Base error for exceptions related with entities, uniquely identified"""
message = "Entity error"
code = statuscode.HTTP_500_INTERNAL_SERVER_ERROR
model = BaseIdentifiedError
def __init__(self, identifier, **kwargs):
super().__init__(identifier=identifier, **kwargs)
class NotFoundException(BaseIdentifiedException):
"""Base error for exceptions raised because an entity does not exist"""
message = "The entity does not exist"
code = statuscode.HTTP_404_NOT_FOUND
model = NotFoundError
class AlreadyExistsException(BaseIdentifiedException):
"""Base error for exceptions raised because an entity already exists"""
message = "The entity already exists"
code = statuscode.HTTP_409_CONFLICT
model = AlreadyExistsError
class ColorConstNotFoundException(NotFoundException):
"""Error raised when a person does not exist"""
message = "The const does not exist"
class ConstNotFoundException(NotFoundException):
"""Error raised when a person does not exist"""
message = "The const does not exist"
class PersonAlreadyExistsException(AlreadyExistsException):
"""Error raised when a person already exists"""
message = "The const already exists"
def get_exception_responses(*args: Type[BaseAPIException]) -> dict:
"""Given BaseAPIException classes, return a dict of responses used on FastAPI endpoint definition, with the format:
{statuscode: schema, statuscode: schema, ...}"""
responses = dict()
for cls in args:
responses.update(cls.response_model())
return responses
+3
View File
@@ -0,0 +1,3 @@
from .auth import *
from .logger_middleware import *
from .prod_middleware import *
+17
View File
@@ -0,0 +1,17 @@
from fastapi import Depends
from starlette.authentication import AuthenticationBackend, AuthCredentials
from starlette.middleware.authentication import AuthenticationMiddleware
from core.utils.auth import get_current_user
class OAuth2Backend(AuthenticationBackend):
async def authenticate(self, request, user=Depends(get_current_user)):
return AuthCredentials(["authenticated"]), user
def setup_auth_middleware(app):
app.add_middleware(
AuthenticationMiddleware,
backend=OAuth2Backend(),
)
+43
View File
@@ -0,0 +1,43 @@
import time
from starlette.requests import Request
from core.app import app
from core.modules import logger
from core.utils.auth import decode_token
@app.middleware("http")
async def logger_middleware(request: Request, call_next):
start_time = time.time()
app.state.request = request
response = await call_next(request)
try:
user = request.state.user if hasattr(request.state, "user") else None
token = (
decode_token(request.headers.get("Authorization"))
if request.headers.get("Authorization")
else None
)
process_time = time.time() - start_time
# logger.emit_access(
# {
# "ip": request.client.host,
# "method": request.method,
# "path": request.url.path,
# "p_time": float("{0:.3f}".format(process_time)),
# "status": response.status_code,
# "uid": token.get("sub") if token else None,
# "scopes": token.get("scopes") if token else None,
# "name": user.name if user else None,
# "level": response.status_code >= 400 and "error" or "info",
# "user_agent": httpagentparser.simple_detect(
# request.headers.get("User-Agent")
# ),
# "referer": request.headers.get("Referer"),
# }
# )
except Exception as e:
logger.emit({'error': str(e)}, logger.Types.Error)
finally:
return response
+20
View File
@@ -0,0 +1,20 @@
from fastapi.requests import Request
from config import conf
from core.app import app
@app.middleware("http")
async def prod_middleware(request: Request, call_next):
if (
request.url.hostname == "scm.haco.dc"
or request.url.hostname == "scm.hacoupian.net"
or request.url.hostname == "rc.hacoupian.net"
):
conf.server.is_prod = True
else:
conf.server.is_prod = False
response = await call_next(request)
response.headers["is-host"] = request.url.hostname
response.headers["is-prod"] = str(conf.server.is_prod)
return response
View File
+167
View File
@@ -0,0 +1,167 @@
import uuid
from datetime import datetime
from typing import Optional, List
import pydantic
from pydantic import Field, create_model
from pydantic.fields import ModelField, FieldInfo
from core.service.mongo_db import use_collection
from core.utils import get_time, get_uuid
MORPH_INPUT_UPDATE = ['updated_at', 'created_at', 'id', 'deleted_at', 'owner_id']
MORPH_INPUT_CREATE = ['updated_at', 'created_at', 'id', 'deleted_at', 'owner_id']
class BaseModel(pydantic.BaseModel):
"""All data models inherit from this class"""
owner_id: Optional[uuid.UUID] = Field(description='ایجاد کننده رکورد')
created_at: Optional[datetime] = Field(description='تاریخ ایجاد رکورد')
updated_at: Optional[datetime] = Field(description='تاریخ آخرین بروزرسانی رکورد')
class At:
update: List[str]
@pydantic.root_validator(pre=True)
def _min_properties(cls, data):
"""At least one property is required"""
if not data:
raise ValueError("At least one property is required")
return data
def dict(self, include_nulls=False, **kwargs):
"""Override the super dict method by removing null keys from the dict, unless include_nulls=True"""
kwargs["exclude_none"] = not include_nulls
result = super().dict(**kwargs)
return result
class Config:
extra = pydantic.Extra.ignore
anystr_strip_whitespace = True # strip whitespaces from strings
underscore_attrs_are_private = False
json_encoders = {
uuid.UUID: lambda v: str(v),
}
@classmethod
def to_partial(cls):
return get_partial_model(cls)
def mongo_dict(self, **kwargs):
result = self.dict(**kwargs)
result['_id'] = result.get('id', None)
result.pop('id', None)
return result
def dict_without_id(self, **kwargs):
result = self.dict(**kwargs)
result.pop('id', None)
return result
def cu_at(self):
self.c_at()
self.u_at()
return self
def c_at(self):
self.created_at = get_time()
return self
def u_at(self):
self.updated_at = get_time()
return self
def init_id(self):
self.id = get_uuid()
return self
def init_owner(self, is_array=False, key="owner_id"):
from core.utils.app_context import use_user
if is_array:
setattr(self, key, [use_user().id if use_user() else None])
else:
setattr(self, key, use_user().id if use_user() else None)
return self
async def save(self):
if self.DB.collection is None:
raise ValueError("collection is not set")
record = await use_collection(self.DB.collection).insert_one(self.u_at().mongo_dict())
if not record.inserted_id:
raise ValueError("Failed to save record")
return record.inserted_id
@classmethod
def at(cls, op: str):
__cols = getattr(cls.At, op)
if __cols:
all_fields = dict()
for key, item in cls.__fields__.items():
if key not in __cols:
if not item.required:
# TODO: must be correct documents
all_fields.setdefault(key, (item.type_, FieldInfo(
default=item.default,
description=item.field_info.description or item.__doc__,
default_factory=item.field_info.default_factory,
alias=item.field_info.alias,
alias_priority=item.field_info.alias_priority,
title=item.field_info.title,
exclude=item.field_info.exclude,
include=item.field_info.include,
const=item.field_info.const,
gt=item.field_info.gt,
ge=item.field_info.ge,
lt=item.field_info.lt,
le=item.field_info.le,
multiple_of=item.field_info.multiple_of,
allow_inf_nan=item.field_info.allow_inf_nan,
max_digits=item.field_info.max_digits,
decimal_places=item.field_info.decimal_places,
min_items=item.field_info.min_items,
max_items=item.field_info.max_items,
unique_items=item.field_info.unique_items,
min_length=item.field_info.min_length,
max_length=item.field_info.max_length,
allow_mutation=item.field_info.allow_mutation,
repr=item.field_info.repr,
regex=item.field_info.regex,
discriminator=item.field_info.discriminator,
extra=item.field_info.extra,
)))
else:
all_fields.setdefault(key, (item.type_, ...))
DynamicModel = create_model(cls.__name__, **all_fields)
return DynamicModel
return cls
class BaseSoftDelete(pydantic.BaseModel):
deleted_at: Optional[datetime] = Field(title='زمان حذف رکورد',
description='زمانی که این فیلد در داده وجود داشته باشد نشان دهنده این است که این رکورد حذف نرم شده است.')
class BaseCUAt(pydantic.BaseModel):
created_at: Optional[datetime]
updated_at: Optional[datetime]
DELETED_AT_QUERY = {'deleted_at': {'$exists': False}}
ORDER_BY_CREATED_AT = {'key_or_list': 'created_at', 'direction': -1}
def get_partial_model(model: BaseModel) -> BaseModel:
"""
Return a model similar to the original, but where all fields are Optional.
Note: this minimal implementation means that many Pydantic features will be discarded
(such as alias). This is not by design and could stand to improve.
"""
new_fields = {name: (Optional[model.__annotations__.get(name)], model.__fields__[
name].default or None) for name in model.__fields__}
return pydantic.create_model(
"PartialModel",
**new_fields,
__base__=BaseModel)
+17
View File
@@ -0,0 +1,17 @@
from bson import ObjectId
class PyObjectId(ObjectId):
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
if not ObjectId.is_valid(v):
raise ValueError("Invalid objectid")
return ObjectId(v)
@classmethod
def __modify_schema__(cls, field_schema):
field_schema.update(type="string")
+7
View File
@@ -0,0 +1,7 @@
from pydantic import BaseModel
class TableEngineInput(BaseModel):
"""
TableEngineInput class
"""
+8
View File
@@ -0,0 +1,8 @@
import uuid
from core.models.base import BaseModel
class UnitAccess(BaseModel):
access: int
unit_id: uuid.UUID
View File
+57
View File
@@ -0,0 +1,57 @@
from enum import Enum
from typing import Union
from fluent import event
from fluent import sender
class Types(Enum):
Default = 'default'
EditData = 'edit_data'
CreateData = 'create_data'
DeleteData = 'delete_data'
Auth = 'authentication'
System = 'system'
Error = 'error'
Upgrade = 'upgrade'
Access = 'access'
def emit(payload: dict, tag: Union[Types, str] = Types.Default) -> None:
sender.setup('mongo', host='localhost', port=24224)
# try:
# owner = useUser().id
# payload['owner'] = str(owner)
# except BaseException:
# payload['owner'] = 'anonymous'
# finally:
val = tag.value if isinstance(tag, Types) else tag
event.Event(val, payload)
def update(dest: str, payload: dict) -> None:
emit({'dest': dest, **payload}, Types.EditData)
def insert(dest: str, payload: dict) -> None:
emit({'dest': dest, **payload}, Types.CreateData)
def delete(dest: str, payload: dict) -> None:
emit({'dest': dest, **payload}, Types.DeleteData)
def access(payload: dict) -> None:
emit(payload, Types.Access)
def auth(payload: dict) -> None:
emit(payload, Types.Auth)
def system(payload: dict) -> None:
emit(payload, Types.System)
def upgrade(payload: dict) -> None:
emit(payload, Types.Upgrade)
+47
View File
@@ -0,0 +1,47 @@
from typing import Generic, TypeVar, Any
from uuid import UUID
from pydantic.generics import GenericModel
from core.service.mongo_db import use_collection
from core.utils.crudify import mongo_inserter, mongo_soft_deleter, mongo_updater, mongo_find_one
from core.utils.table_engine import TableEngineRequestType, TableEngineExport
from core.utils.te import NewTableEngine
T = TypeVar('T')
class BaseRepository(GenericModel, Generic[T]):
COLLECTION: str = 'asa'
FETCH_COLS_SEARCH = ['v_id']
MODEL: T = None
def __init__(self, collection: str, fetch_cols_search=None, model: T = None, **data: Any):
super().__init__(**data)
if fetch_cols_search is None:
fetch_cols_search = ['v_id']
self.COLLECTION = collection
self.MODEL = model
self.FETCH_COLS_SEARCH = fetch_cols_search
async def get(self, _id: UUID) -> T:
return await mongo_find_one(_id, self.COLLECTION)
async def fetch(self, _input: TableEngineRequestType) -> TableEngineExport[T]:
return await NewTableEngine.mongo(
query_class=use_collection(self.COLLECTION),
soft_deleted=True,
model=self.MODEL,
search_cols=self.FETCH_COLS_SEARCH,
inputs=_input.te) \
.to_dict()
async def create(self, create: T) -> T:
return await mongo_inserter(create, self.COLLECTION, getter=self.get)
async def update(self, _id: UUID, update: T, extra_query: dict = None):
return await mongo_updater(update, _id, self.COLLECTION, getter=self.get,
extra_query=extra_query)
async def delete(self, _id: UUID):
return await mongo_soft_deleter(_id, self.COLLECTION)
+15
View File
@@ -0,0 +1,15 @@
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from starlette.requests import Request
from config import conf
from core.app import app
def get_unique_identification(request: Request) -> str:
return request.client.host or "127.0.0.1"
limiter = Limiter(key_func=get_unique_identification, storage_uri=conf.database.get('limiter_redis').url)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
+14
View File
@@ -0,0 +1,14 @@
import motor.motor_asyncio
from pymongo.collection import Collection
from core.utils.mongo import get_mongo_url, get_mongo_db
def use_client(connection_name: str = None):
return motor.motor_asyncio.AsyncIOMotorClient(get_mongo_url(connection_name))
def use_collection(collection: str, db: str = None, connection_name: str = None) -> Collection:
if not db:
db = get_mongo_db(connection_name)
return use_client(connection_name)[db][collection]
+34
View File
@@ -0,0 +1,34 @@
import json
import aioredis
from aioredis import Redis
from aioredis.client import ExpiryT
from core.config import DatabaseConfig
def redis_db(db: DatabaseConfig) -> Redis:
# Redis client bound to pool of connections (auto-reconnecting).
redis = aioredis.from_url(
db.url,
encoding="utf-8",
decode_responses=True,
db=int(db.db) or 0
)
return redis
async def redis_set(key: str, value: any, db: DatabaseConfig, ex=ExpiryT):
redis_c = redis_db(db)
await redis_c.set(key, json.dumps(value), ex=ex)
await redis_c.close()
return True
async def redis_get(key: str, db: DatabaseConfig):
redis_c = redis_db(db)
_return = await redis_c.get(key)
await redis_c.close()
if not _return or isinstance(_return, (int, float, complex)):
return _return
return json.loads(_return)
+1
View File
@@ -0,0 +1 @@
from .sumery import *
+38
View File
@@ -0,0 +1,38 @@
from typing import List
from fastapi import Request
from pydantic import create_model
from config import conf
from core.app import app
from core.utils.router import generate_router, safe_route
from core.utils.sumery import sumery_executor, sumery_default_getter
router = generate_router("com/sumery", auth_dependencies=True, version='v2')
_tag_base = ['sumery']
_pre = conf.server.prefix_url.replace('v1', 'v2')
@router.get('/{_object}', response_model=List, tags=_tag_base)
async def smart_query(q: str, _object: str, request: Request):
params = request.query_params
extra_filter = {}
for key, value in params.items():
if key.startswith('e_'):
extra_filter.setdefault(key[2:], value)
return await safe_route('get', sumery_executor, _object, q, extra_filter)
@router.post('/default/{_object}', response_model=List, tags=_tag_base)
async def smart_query(_object: str, _input: create_model('InputDefaultSumery', ids=[]), request: Request):
params = request.query_params
extra_filter = {}
for key, value in params.items():
if key.startswith('e_'):
extra_filter.setdefault(key[2:], value)
return await safe_route('get', sumery_default_getter, _object, _input.ids, extra_filter)
app.include_router(router)
+8
View File
@@ -0,0 +1,8 @@
import os
from .utils import *
from .response import *
def get_path(__file__=__file__):
return os.path.abspath(os.path.dirname(__file__)) + '/'
+5
View File
@@ -0,0 +1,5 @@
from config import conf
def get_namespace_api(prefix):
return f"{conf.server.prefix_url}/{prefix}"
+21
View File
@@ -0,0 +1,21 @@
from starlette.requests import Request
from core.app import app
from core.utils.auth import UserModel
def use_request() -> Request:
try:
return app.state.request.state
except AttributeError:
raise AttributeError("No request object found in app.state")
def use_user(user=None) -> UserModel:
try:
if user is None:
return use_request().user
else:
app.state.request.state.user = user
except AttributeError:
raise AttributeError("No user object found in app.state")
+78
View File
@@ -0,0 +1,78 @@
import httpx
from fastapi import HTTPException, Depends, Security
from fastapi.security import SecurityScopes
from httpx import ConnectError
from jose import jwt
from starlette.requests import Request
from config import conf
from .model import UserModel, ValidateTokenInput
from .schema import oauth2
from ..app_context import use_user
from ...service.redis import redis_set, redis_get
def decode_token(token: str):
return jwt.decode(
token.split(' ').pop(), conf.auth.secret_key, algorithms=[conf.auth.algorithm])
async def validate_token(_input: ValidateTokenInput):
try:
_cache_key = _input.token.split('.')[-1]
_cache_auth = await redis_get(f'{conf.auth.cache_prefix_key}{_cache_key}', conf.database.get('auth_redis'))
if not _cache_auth:
client = httpx.AsyncClient()
headers = {'Content-Type': 'application/json', }
result = await client.request('POST', conf.auth.url, json=_input.dict(), headers=headers)
export = result.text
if 'application/json' in result.headers['content-type']:
export = result.json()
if result.status_code > 299:
raise HTTPException(status_code=result.status_code, detail=export)
if 'v2' in conf.auth.url:
export = export.get('data')
await redis_set(f'{conf.auth.cache_prefix_key}{_cache_key}', export, conf.database.get('auth_redis'),
ex=conf.auth.cache_ttl)
return UserModel(**export)
else:
return UserModel(**_cache_auth)
except ConnectError as e:
raise HTTPException(status_code=400, detail=str(e))
except HTTPException as e:
raise e
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2)):
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = f"Bearer"
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": authenticate_value},
)
user = await validate_token(ValidateTokenInput(token=token, scopes=security_scopes.scopes))
if user is None:
raise credentials_exception
user.scopes = security_scopes.scopes if isinstance(
security_scopes.scopes, list) else security_scopes.scopes
return user
async def get_current_active_user(request: Request,
current_user: UserModel = Security(get_current_user, scopes=["account"]), ):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
request.state.user = UserModel(**current_user.mongo_dict())
use_user(UserModel(**current_user.mongo_dict()))
return current_user
+29
View File
@@ -0,0 +1,29 @@
import uuid
from typing import Optional, Union, List
import pydantic
from pydantic import Field
from core.models.base import BaseModel
class UserModel(BaseModel):
id: Optional[Union[uuid.UUID, str]] = Field(alias='_id')
username: Optional[str]
mobile: Optional[str]
name: Optional[str]
first_name: Optional[str]
last_name: Optional[str]
is_su: Optional[bool]
unit: Optional[str]
avatar: Optional[str]
desc: Optional[str]
scopes: Optional[List[str]]
home_path: Optional[str]
roles: Optional[List[uuid.UUID]]
disabled: Optional[bool]
class ValidateTokenInput(pydantic.BaseModel):
token: str
scopes: Optional[List[str]] = ['account']
+9
View File
@@ -0,0 +1,9 @@
from fastapi.security import OAuth2PasswordBearer
oauth2 = OAuth2PasswordBearer(
tokenUrl="token",
scopes={
"account": "Read information about the current user.",
"scm": "Scm Modules",
},
)
+18
View File
@@ -0,0 +1,18 @@
from functools import wraps
from typing import List, Union
from core.utils.app_context import use_user
def can(permissions: Union[str, List[str]]):
def can_inner(func):
@wraps(func)
async def wrapper(*args, **kwargs):
print(permissions)
user = use_user()
# raise HTTPException(status_code=403, detail='forbidden request')
return await func(*args, **kwargs)
return wrapper
return can_inner
+118
View File
@@ -0,0 +1,118 @@
from dataclasses import dataclass
from uuid import UUID
from core.exceptions import ModifiedCountException, RecordNotFoundException
from core.models.base import BaseModel, DELETED_AT_QUERY
from core.service.mongo_db import use_collection
from core.utils import get_time, get_v_id
@dataclass
class ConfigMongoWriter:
created_at: bool = True
updated_at: bool = True
has_owner: bool = True
init_id: bool = True
init_v_id: bool = False
async def mongo_inserter(model: BaseModel, collection: str, options: ConfigMongoWriter = ConfigMongoWriter(),
getter=None, db=None, v_id_prefix=None, client=None) -> dict | BaseModel:
"""
Inserts data into collection
"""
try:
if options.init_id:
model.init_id()
if options.has_owner:
model.init_owner()
if options.created_at and options.updated_at:
model.cu_at()
if options.created_at:
model.c_at()
if options.updated_at:
model.u_at()
if options.init_v_id:
model.v_id = get_v_id(v_id_prefix or 'uk', source_id=model.id)
document = model.mongo_dict()
if client:
result = await client(collection).insert_one(document)
else:
result = await use_collection(collection, db).insert_one(document)
assert result.acknowledged
if getter:
return await getter(result.inserted_id)
return result
except AssertionError as e:
raise AssertionError(f'{collection} insert error')
except Exception as e:
raise e
async def mongo_soft_deleter(query: dict | UUID, collection: str, db=None, client=None):
"""
Updates data in collection
"""
# doc = {"$set": {'deleted_at': get_time()}, }
_query = query
if isinstance(query, UUID):
_query = {'_id': query}
__trash_collection = f'{collection}--trash'
__driver = client or use_collection
__current_record = await __driver(collection, db=db).find_one(_query)
if not __current_record:
raise RecordNotFoundException(identifier=str(query))
__current_record['deleted_at'] = get_time()
__driver(__trash_collection, db=db).insert_one(__current_record)
__driver(collection, db=db).delete_one(_query)
return True
async def mongo_updater(model: BaseModel,
query: dict | UUID,
collection: str,
options: ConfigMongoWriter = ConfigMongoWriter(created_at=False, has_owner=False),
getter=None,
db=None,
extra_query: dict = None,
client=None,
):
"""
Updates data in collection
"""
_query = query
if isinstance(query, UUID):
_query = {'_id': query}
if options.has_owner:
model.init_owner()
if options.updated_at:
model.u_at()
if client:
result = await client(collection).update_one(_query, {"$set": model.dict(),
**(extra_query if extra_query else {})})
else:
result = await use_collection(collection, db).update_one(_query, {"$set": model.dict(),
**(extra_query if extra_query else {})})
if not result.modified_count:
raise ModifiedCountException(query)
if getter:
return await getter(_query)
return result
async def mongo_find_one(query: dict | UUID, collection: str, db=None, client=None, project=None) -> dict:
"""
Ge
"""
_query = query
if isinstance(query, UUID):
_query = {'_id': query}
_query = dict(DELETED_AT_QUERY, **_query)
if client:
result = await client(collection).find_one(_query, project)
else:
result = await use_collection(collection, db).find_one(_query, project)
if not result:
raise RecordNotFoundException(identifier=query.__str__())
return result
+42
View File
@@ -0,0 +1,42 @@
import json
from datetime import datetime
from typing import Optional, Any, Union
from uuid import UUID
#
# def uuid_encoder():
# print(obj)
# if isinstance(obj, UUID):
# # if the obj is uuid, we simply return the value of uuid
# return obj.hex
# return json.JSONEncoder.default(self, obj)
#
import jsonpatch
from pydantic import Field
from core.models.base import BaseModel
class DataModelPatch(BaseModel):
id: Optional[Union[UUID, str]] = Field(alias='_id')
patch: Optional[Any]
ref_id: UUID
collection: str
owner: UUID
created_at: Optional[datetime]
class DB:
collection: str = 'revisions'
async def set_revision(collection: str, old: str | dict, new: str | dict, ref_id: UUID, user_id: UUID):
old = json.dumps(old, default=str) if isinstance(old, dict) else old
new = json.dumps(new, default=str) if isinstance(new, dict) else new
patch = jsonpatch.JsonPatch.from_diff(
json.loads(new), json.loads(old)
)
await DataModelPatch(
patch=json.loads(patch.to_string()),
ref_id=str(ref_id),
collection=collection,
owner=str(user_id)).init_id().c_at().save()
+47
View File
@@ -0,0 +1,47 @@
from typing import List, Dict, Any
from pymongo.collection import Collection
from config import conf
from core.exceptions import NotFoundException
from core.utils import get_time
def get_mongo_url(connection_name:str = None):
return conf.server.is_prod and conf.database.get(connection_name or 'mongo').production.url or conf.database.get(
connection_name or 'mongo').dev.url
def get_mongo_db(connection_name:str = None):
return conf.server.is_prod and conf.database.get(connection_name or 'mongo').production.db or conf.database.get(
connection_name or 'mongo').dev.db
def nested_join(collection: str, from_field, to_field,
alias, match: dict) -> List[Dict[str, Any]]:
"""
Join a list of nested dictionaries into a single dictionary.
"""
return [
{
"$lookup": {
"from": collection,
"localField": from_field,
"foreignField": to_field,
"as": alias
},
},
{
"$match": match if match else {}
}
]
async def use_soft_delete(collection: Collection, find_query: dict) -> bool:
"""
Soft delete a document by setting a field to true.
"""
result = await collection.update_one(find_query, {"$set": {'deleted_at': get_time()}, })
if not result.modified_count:
raise NotFoundException(f"No document found with query: {find_query}")
return True
+12
View File
@@ -0,0 +1,12 @@
import re
E2P_map = {'1': '۱', '2': '۲', '3': '۳', '4': '۴', '5': '۵', '6': '۶', '7': '۷', '8': '۸', '9': '۹', '0': '۰'}
E2P_map = {'1': '۱', '2': '۲', '3': '۳', '4': '۴', '5': '۵', '6': '۶', '7': '۷', '8': '۸', '9': '۹', '0': '۰'}
# convert persian number to english number
def convert_number_to_persian(str_in: str):
a = map(lambda ch: E2P_map[ch] if ch in E2P_map else ch, str_in)
return ''.join(list(a))
+163
View File
@@ -0,0 +1,163 @@
from fastapi import HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic import ValidationError
from starlette.responses import JSONResponse
from core.exceptions import SoftDeletedException, RecordNotFoundException, ModifiedCountException, ValidationException
class ResponseModel:
def __init__(self, data, code=200, message="ok"):
self.data = data
self.code = code
self.message = message
def __str__(self):
return str(self.data)
def __iter__(self):
return {
"data": self.data,
"code": self.code,
"message": self.message,
}
class ResponseBadRequest(ResponseModel):
def __init__(self, data, message="Bad Request"):
super().__init__(data, 400, message)
class ResponseValidationRequest(ResponseModel):
def __init__(self, data, message="Bad Request"):
super().__init__(data, 422, message)
class ResponseWriteErrorRequest(ResponseModel):
def __init__(self, data, message="Request write error"):
super().__init__(data, 400, message)
class ResponseNotFound(ResponseModel):
def __init__(self, data, message="Not Found"):
super().__init__(data, 404, message)
class ResponseUnauthorized(ResponseModel):
def __init__(self, data, message="Unauthorized"):
super().__init__(data, 401, message)
class ResponseInternalServerError(ResponseModel):
def __init__(self, data, message="Internal Server Error"):
super().__init__(data, 500, message)
class ResponseOk(ResponseModel):
def __init__(self, data, code=200, message="ok"):
super().__init__(data, code, message)
class ResponseOkUpdate(ResponseOk):
def __init__(self, data, message="update was successful"):
super().__init__(data, 200, message)
class ResponseOkCreate(ResponseOk):
def __init__(self, data, message="create was successful"):
super().__init__(data, 201, message)
class Res:
@staticmethod
def json(object, status_code):
return JSONResponse(content=jsonable_encoder(object), status_code=status_code)
@staticmethod
def create(data, message="create was successful"):
return Res.json(ResponseOkCreate(data, message), 201)
@staticmethod
def fetch(data, message="fetch was successful"):
return Res.json(ResponseOk(data, 200, message), 200)
@staticmethod
def update(data, message="update was successful"):
return Res.json(ResponseOkUpdate(data, message), 200)
@staticmethod
def not_found(data, message="Not Found"):
return Res.json(ResponseNotFound(data, message), 404)
@staticmethod
def unauthorized(data, message="Unauthorized"):
return Res.json(ResponseUnauthorized(data, message), 401)
@staticmethod
def server_error(data, message="Internal Server Error"):
return Res.json(ResponseInternalServerError(data, message), 500)
@staticmethod
def bad_request(data, message="Bad Request"):
return Res.json(ResponseBadRequest(data, message), 400)
@staticmethod
def validation_error(data, message="data validation error"):
return Res.json(ResponseValidationRequest(data, message), 422)
@staticmethod
def write_error(data, message="Request write error"):
return Res.json(ResponseWriteErrorRequest(data, message), 400)
@staticmethod
def delete_error(data, message="Request delete error"):
return Res.json(ResponseWriteErrorRequest(data, message), 400)
def to_http_res(res, _type, message=None):
if _type == "create":
return Res.create(res, "create was successful")
elif _type == "fetch":
return Res.fetch(res, "fetch was successful")
elif _type == "update":
return Res.update(res, "update was successful")
elif _type == "delete":
return Res.update(res, "delete was successful")
elif _type == "get":
return Res.fetch(res, "get was successful")
elif _type == "upload":
return Res.fetch(res, "upload complete")
elif _type == "token":
return Res.fetch(res, "token was generated")
else:
return Res.fetch(res, "response ok")
def http_exception(res):
_return = None
# if isinstance(res, ResponseBadRequest):
# _return = HTTPException(status_code=res.code, detail=res.message)
# elif isinstance(res, ResponseNotFound):
# _return = HTTPException(status_code=res.code, detail=res.message)
# elif isinstance(res, ResponseUnauthorized):
# _return = HTTPException(status_code=res.code, detail=res.message)
# elif isinstance(res, ResponseInternalServerError):
# _return = HTTPException(status_code=res.code, detail=res.message)
if isinstance(res, SoftDeletedException):
_return = Res.delete_error(res, res.message)
elif isinstance(res, RecordNotFoundException):
_return = Res.not_found(res, res.message)
elif isinstance(res, ModifiedCountException):
_return = Res.write_error(res, res.message)
elif isinstance(res, ValidationError):
_return = Res.validation_error(res.errors())
elif isinstance(res, AssertionError):
_return = Res.validation_error(res, message=res)
elif isinstance(res, ValidationException):
_return = Res.validation_error(res.message, message=res.message)
elif isinstance(res, HTTPException):
_return = Res.bad_request(res.status_code, message=res.detail)
else:
_return = Res.server_error(res, f'unknown error: {str(res)} {type(res)}')
return _return
+206
View File
@@ -0,0 +1,206 @@
import time
from functools import wraps
from typing import Callable, TypeVar, Generic
from fastapi import APIRouter, Request, Response, Body
from fastapi import Security
from fastapi.routing import APIRoute
from starlette.background import BackgroundTasks
from auth.utils import get_current_active_user
from config import conf
from core.utils import to_http_res, http_exception
from core.utils.auth import get_current_active_user
class BaseRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
before = time.time()
response: Response = await original_route_handler(request)
duration = time.time() - before
response.headers["X-Response-Time"] = str(duration)
return response
return custom_route_handler
def shrink(cal: Callable, name: str = None, bt: BackgroundTasks = None):
def can_inner(func):
@wraps(func)
async def wrapper(*args, **kwargs):
return await safe_route(name or cal.__name__, cal, *kwargs.values())
return wrapper
return can_inner
def back_task(cal: Callable, name: str = None):
print(cal)
def back_inner(func):
print(func)
@wraps(func)
async def wrapper(*args, **kwargs):
if cal.__name__ == 'create_salam':
print(cal.__name__)
return await safe_route(name or cal.__name__, cal, *kwargs.values())
return wrapper
return back_inner
def generate_router(
prefix,
dependencies=None,
auth_dependencies=False,
version='v1',
auth_scopes=['account']):
"""
Generate a router with a prefix and dependencies.
Args:
prefix: prefix of the router.
dependencies: dependencies of the router.
auth_dependencies: if router has auth dependencies is True.
version: version of the router.
auth_scopes: auth scopes of the router.
Returns: ApiRouter
"""
if auth_dependencies:
dependencies = dependencies or []
if 'customer' in auth_scopes:
dependencies.extend(
[Security(get_current_active_customer, scopes=auth_scopes)])
else:
dependencies.extend(
[Security(get_current_active_user, scopes=auth_scopes)])
_pre = conf.server.prefix_url.replace('v1', version)
return APIRouter(
prefix=f"{_pre}/{prefix}",
route_class=BaseRoute,
dependencies=dependencies if dependencies else []
)
async def safe_route(_type, fn, *args):
try:
return to_http_res(await fn(*args), _type)
except Exception as e:
return http_exception(e)
class CrudMethods:
create = True
read = True
update = True
delete = True
get_all = True
def __init__(self, create=True, read=True, update=True, delete=True, fetch=True):
self.create = create
self.read = read
self.update = update
self.delete = delete
self.fetch = fetch
def __iter__(self):
r = []
if self.create:
r.append('create')
if self.read:
r.append('read')
if self.update:
r.append('update')
if self.delete:
r.append('delete')
if self.fetch:
r.append('fetch')
return r
T = TypeVar("T")
class SetupCrud(Generic[T]):
_url_prefix = {
'create': '/create',
'get': '/{_id}',
'update': '/{_id}',
'delete': '/{_id}',
'fetch': ''
}
_methods = {
'create': 'post',
'fetch': 'post',
'get': 'get',
'update': 'put',
'delete': 'delete',
}
def __init__(self,
router: APIRouter,
repository,
model: T,
tags=[],
methods: CrudMethods = CrudMethods()
):
for i in ['create', 'update', 'get', 'delete', 'fetch']:
if i == 'create':
@router.post(
self._url_prefix[i],
response_model=model,
tags=tags
)
async def create(body=Body(...)):
b = T(**body)
return await safe_route(i, getattr(repository, 'create'), b)
# elif i == 'update':
# async def update(_id, body: T = Body(...)):
# return await safe_route(i, getattr(repository, i), _id, body)
#
# router.add_api_route(
# _url_prefix[i],
# update,
# methods=[_methods[i]],
# response_model=model,
# tags=tags
# )
# elif i == 'delete':
# async def delete(_id):
# return await safe_route(i, getattr(repository, i), _id)
#
# router.add_api_route(
# _url_prefix[i],
# delete,
# methods=[_methods[i]],
# response_model=model,
# tags=tags
# )
# elif i == 'get':
# async def get(_id):
# return await safe_route(i, getattr(repository, i), _id)
#
# router.add_api_route(
# _url_prefix[i],
# get,
# methods=[_methods[i]],
# response_model=model,
# tags=tags
# )
# elif i == 'fetch':
# async def fetch(_id, body: T = Body(...)):
# return await safe_route(i, getattr(repository, i), _id, body)
#
# router.add_api_route(
# _url_prefix[i],
# fetch,
# methods=[_methods[i]],
# response_model=model,
# tags=tags
# )
+89
View File
@@ -0,0 +1,89 @@
"""
smart upon mini query
amazing mini query in any database
"""
from core.models.base import DELETED_AT_QUERY
from core.utils import smart_cast
from core.service.mongo_db import use_collection
from core.exceptions import ValidationException
class SumeryObject:
db: str
object_name: str
table_name: str
driver: str = 'mongo'
query_fn: callable
cols_search: list
key_col: str = '_id'
value_col: str
value_fn: callable
limit: 10
def __init__(self, db: str,
object_name: str,
cols_search: list,
key_col: str,
value_col: str,
driver: str = 'mongo',
table_name: str = None,
query_fn: callable = None,
limit: int = 10,
value_fn: callable = None,
):
self.db = db
self.table_name = table_name
self.object_name = object_name
self.driver = driver
self.query_fn = query_fn
self.cols_search = cols_search
self.key_col = key_col
self.value_col = value_col
self.value_fn = value_fn
self.limit = limit
__sumery_list: list[SumeryObject] = []
async def sumery_executor(object_name: str, query: str, extra_filter: dict = None):
__query = query.strip()
if len(__query) < 3:
raise ValidationException('length of query not valid')
_so = next((x for x in __sumery_list if x.object_name == object_name), None)
if not _so:
raise ValidationException('summery object not found')
_query = {'$and': [{'$or': [{x: {'$regex': __query}} for x in _so.cols_search]},
DELETED_AT_QUERY,
extra_filter if extra_filter else {}]}
_projection = {
'_id': False,
'key': f'${_so.key_col}',
'value': f'${_so.value_col}'
}
records = use_collection(_so.table_name or _so.object_name, _so.db).find(_query, _projection).limit(_so.limit)
return [x async for x in records]
async def sumery_default_getter(object_name: str, ids: list, extra_filter: dict = None):
if not len(ids):
raise ValidationException('length of ids not valid')
_so = next((x for x in __sumery_list if x.object_name == object_name), None)
if not _so:
raise ValidationException('summery object not found')
__cast_ids = [smart_cast(x) for x in ids]
_query = {'$and': [{_so.key_col: {'$in': __cast_ids}},
DELETED_AT_QUERY,
extra_filter if extra_filter else {}]}
_projection = {
'_id': False,
'key': f'${_so.key_col}',
'value': f'${_so.value_col}'
}
records = use_collection(_so.table_name or _so.object_name, _so.db).find(_query, _projection).limit(100)
return [x async for x in records]
def setup_sumery_object(so: SumeryObject):
__sumery_list.append(so)
+4
View File
@@ -0,0 +1,4 @@
from .engine import *
from .filter import *
from .input import *
from .types import *
+112
View File
@@ -0,0 +1,112 @@
from pydantic import BaseModel
from sqlalchemy import func
from core.models.base import ORDER_BY_CREATED_AT
from core.utils.table_engine.filter import TableEngineFilter
from core.utils.table_engine.input import TableEngineInput
class TableEngine:
object: any
input: TableEngineInput
is_sql_server: bool
is_deleted: bool
model: any
joins: list = None
filter: TableEngineFilter
sort: dict = ORDER_BY_CREATED_AT
extra_filter: list
def __init__(self, _object, _input: dict):
self.object = _object
_input = _input or {}
self.input = TableEngineInput(**_input)
self.input.get_limit()
self.filter = TableEngineFilter(self.input)
self.extra_filter = []
def soft_deleted(self):
self.is_deleted = True
self.input.is_deleted = True
return self
def sql_server(self):
self.is_sql_server = True
return self
def text_search_cols(self, cols: list):
self.input.text_search_cols(cols)
return self
def set_limit(self, limit: int):
self.input.p.s = limit
return self
def set_model(self, model: BaseModel.__class__):
self.model = model
return self
def join(self, joins):
self.joins = joins
return self
def sort_by(self, sort_by: dict | str):
self.sort = sort_by
return self
def _get_sql_order(self):
if isinstance(self.sort, dict):
return self.model.Id.desc()
return self.sort
def add_filter(self, filters):
self.extra_filter.append(filters)
self.input.extra_filter.append(filters)
return self
def get_query(self):
return self.input.dict()
async def exec(self):
if hasattr(self, 'is_sql_server') and self.is_sql_server:
return await self._exec_sql_server()
else:
return await self._exec_mongo()
async def _exec_mongo(self):
cursor = self.object.find(
self.filter.get()).sort(
**self.sort).limit(
self.input.get_limit()).skip(self.input.skip())
return [self.model(**document) async for document in cursor]
async def _exec_sql_server(self):
query = self.object \
.query(self.model)
if self.joins:
for j in self.joins:
if isinstance(j, list):
query = query.join(*j)
else:
query = query.join(j)
query = self.filter.get_sql_server(
self.model, query, self.extra_filter)
count = query.count()
query = query \
.order_by(self._get_sql_order()) \
.offset(self.input.skip()) \
.limit(self.input.get_limit())
return query.all(), count
async def count(self):
if hasattr(self, 'is_sql_server') and self.is_sql_server:
count = self.object.query(func.count(self.model.Id))
else:
count = await self.object.count_documents(
self.filter.get())
return count
+160
View File
@@ -0,0 +1,160 @@
__all__ = ['TableEngineFilter']
from uuid import UUID
from sqlalchemy import or_
from core.utils.table_engine.input import TableEngineInput, FilterItem
from core.utils.utils import is_valid_uuid
def filter_item_generator(item: FilterItem) -> dict:
if is_valid_uuid(item.col):
return {
'data': {
"$elemMatch": {
'field_id': UUID(item.col),
'value': {**get_expression_filter(item.op, cast_value(item.val, item.type))}
}
}
}
return {item.col: {
**get_expression_filter(item.op, cast_value(item.val, item.type))}}
def cast_value(val: str, type: str) -> any:
if type in ['NUMBER', 'STACK', 'FLOAT']:
return float(val)
if type == 'UUID':
return UUID(str(val))
return val
def get_expression_filter(op: str, val: str) -> dict:
_op = get_operator(op)
match _op:
case "sw":
return {'$regex': f'^{val}'}
case "ew":
return {'$regex': f'{val}$'}
case "n_ew":
return {'$regex': f'(?<!{val})$'}
case "n_sw":
return {'$regex': f'^(?!{val})'}
case "n_like":
return {'$not': {'$regex': val}}
case "bet":
if type(val) is str:
return {'$gte': val.split('-')[0], '$lte': val.split('-')[1]}
return {'$gte': val[0], '$lte': val[1]}
case "n_bet":
if type(val) is str:
return {'$gte': val.split('-')[0], '$lte': val.split('-')[1]}
return {'$not': {'$gte': val[0], '$lte': val[1]}}
case "exi":
return {'$exists': True}
case "n_exi":
return {'$exists': False}
case "in":
return {'$exists': True}
case "not_in":
return {'$exists': False}
return {_op: val}
def get_operator(op: str) -> str:
ops = {
"eq": "$eq",
"n_eq": "$ne",
"gt": "$gt",
"ge": "$ge",
"lt": "$lt",
"le": "$le",
"like": "$regex",
"n_like": "n_like",
"sw": "sw",
"ew": "ew",
'n_ew': 'n_ew',
'n_sw': 'n_sw',
'exi': 'exi',
"n_exi": "n_exi",
'lte': '$lte',
'gte': '$gte',
'in': 'in',
'n_in': 'not_in',
'bet': 'bet',
'n_bet': 'n_bet',
}
return ops[op] if op in ops else '$eq'
def getAndOr(op: str) -> str:
return {"and": "$and", "or": '$or'}.get(op, '$and')
class TableEngineFilter:
stack: list
input: TableEngineInput
def __init__(self, _input: TableEngineInput):
self.stack = []
self.input = _input
def add(self, filter_item: dict):
self.stack.append(filter_item)
def query_generator(self):
if not self.input.has_filter():
return []
filters = self.input.filters()
query = []
for filter_scope in filters:
query.append({getAndOr(filter_scope.op): [
filter_item_generator(item) for item in filter_scope.items]})
return query
def get(self):
f = self.stack
query = self.query_generator()
if query:
f.append(*query)
if self.input.has_text_search():
items = {}
for col in self.input.get_search_cols():
items[col] = {'$regex': self.input.get_text_search()}
f.append(items)
if hasattr(self.input, 'is_deleted') and self.input.is_deleted:
f.append({'deleted_at': {'$exists': False}})
if len(self.input.extra_filter) > 0:
f.extend(self.input.extra_filter)
if not f:
return {}
return {'$and': f}
def get_sql_server(self, model, query, filters):
"""
text search filter
"""
text_search_stack = []
fi = []
if self.input.has_text_search():
for col in self.input.get_search_cols():
if isinstance(col, str):
text_search_stack.append(getattr(model, col).like(
f'%{self.input.get_text_search()}%'))
if text_search_stack:
fi.append(or_(*text_search_stack))
if self.input.is_deleted:
fi.append(getattr(model, 'IsDeleted') == 0)
if filters:
for fil in filters:
fi.append(fil)
query = query.filter(*fi)
return query
+159
View File
@@ -0,0 +1,159 @@
from enum import Enum
from typing import List, Optional, Any
import pydantic
from pydantic import BaseModel
class Cr(str, Enum):
OR = 'or'
AND = 'and'
class FilterItem(BaseModel):
col: Optional[str]
cr: Optional[Cr]
id: Optional[str]
op: Optional[str]
type: Optional[str]
val: Optional[Any]
@staticmethod
def ValidItem(item: dict):
return item.get('col') and \
item.get('id') and \
item.get('op') and \
item.get('type')
@staticmethod
def ValidValue(item: dict):
return len(str(item.get('val', '')).strip()) > 0
class TableEngineInput(BaseModel):
class FilterScope(BaseModel):
id: str
items: Optional[List[FilterItem]]
op: Optional[Cr]
def __init__(self, **kwargs):
super().__init__(**kwargs)
if kwargs.get('items'):
self.items = [
FilterItem(
**item) for item in filter(
lambda i: FilterItem.ValidItem(i),
kwargs.get(
'items',
[]))]
self.op = Cr(kwargs.get('op'))
self.id = str(kwargs.get('id'))
@staticmethod
def ValidScope(item: dict):
return item.get('id') and item.get('items') and len(
item.get('items')) > 0 and item.get('op')
@staticmethod
def CleanItems(item):
clone = item.dict()
clone['items'] = list(filter(
lambda i: FilterItem.ValidValue(i), clone.get(
'items', [])))
return TableEngineInput.FilterScope(**clone)
class Page(BaseModel):
c: int = 1
s: int = 5
class SearchModel(BaseModel):
text: Optional[str]
cols: Optional[List[Any]]
class SortModel(BaseModel):
field: Optional[str]
direction: Optional[str]
class Config:
extra = pydantic.Extra.ignore
anystr_strip_whitespace = True # strip whitespaces from strings
underscore_attrs_are_private = False
arbitrary_types_allowed = True
f: Optional[List[FilterScope]]
extra_filter: list = []
o: Optional[SortModel]
p: Optional[Page] = Page()
s: Optional[SearchModel]
is_deleted: Optional[bool]
def limit(self):
return self.p.s
def order(self):
return self.p.o
def filters(self) -> List[FilterScope]:
return self.f
def has_filter(self) -> bool:
return self.f is not None and \
len(self.f) > 0 and \
self.f[0].items is not None and \
len(self.f[0].items) > 0
def has_extra_filter(self) -> bool:
return self.extra_filter is not None and \
len(self.extra_filter) > 0
def scopes(self):
return self.s
def __init__(self, **kwargs):
super().__init__(**kwargs)
_input = {**{'p': {'c': 1, 's': 5},
'f': [], 's': {}, 'o': {}}, **(kwargs or {})}
self.p = TableEngineInput.Page(**_input.get('p'))
if _input.get('f'):
self.f = [
TableEngineInput.FilterScope(
**f) for f in filter(
lambda fs:TableEngineInput.FilterScope.ValidScope(fs),
_input.get('f'))]
self.f = list(map(
lambda fs: TableEngineInput.FilterScope.CleanItems(fs),
self.f))
if _input.get('s'):
self.s = TableEngineInput.SearchModel(**_input.get('s'))
def skip(self):
return self.page() * self.p.s
def page(self):
return self.p.c - 1
def virtual_page(self):
return self.page() + 1
def get_limit(self):
return self.p.s if self.p else 20
def text_search_cols(self, cols: List[str]):
if self.s:
self.s.cols = cols
def has_text_search(self) -> bool:
return self.s is not None and self.s.text is not None and self.s.cols is not None and len(
self.s.cols) > 0 and len(self.s.text.strip()) > 0
def add_filter(self, _filter):
self.extra_filter.append(_filter)
def get_search_cols(self) -> List[str]:
return self.s.cols if self.s else []
def get_text_search(self) -> str:
return self.s.text.strip() if self.s else None
+23
View File
@@ -0,0 +1,23 @@
from typing import Generic, TypeVar, List, Optional
from pydantic.generics import GenericModel
T = TypeVar('T')
class TableEngineInputType(GenericModel, Generic[T]):
f: Optional[List]
o: Optional[dict] = {}
p: Optional[dict] = {}
s: Optional[dict] = {}
class TableEngineExport(GenericModel, Generic[T]):
rows: Optional[List[T]]
total: Optional[int]
page: Optional[int]
te: Optional[TableEngineInputType]
class TableEngineRequestType(GenericModel, Generic[T]):
te: Optional[TableEngineInputType]
+77
View File
@@ -0,0 +1,77 @@
from typing import List
from .engines import MsSqlEngine, MongoEngine, PgSqlEngine, AbstractEngine
from .utils.Models import TableEngineInput
from .utils.Models import WorkingMode, JoinModel
from ..table_engine import TableEngineExport
class NewTableEngine:
def __init__(self, query_class, model, inputs, mode, engine: AbstractEngine, extra_filter,
default_sort_by, joins, search_cols, options=None, soft_deleted=False):
self.model = model
self.query_class = query_class
self.mode = mode
self.joins = False
self.raw_input = inputs
if isinstance(inputs, dict):
self.input = TableEngineInput(**inputs if inputs else {})
else:
self.input = TableEngineInput(**inputs.dict() if inputs else {})
self.input.extra_filter = extra_filter
self.default_sort_by = default_sort_by
self.joins = joins
self.soft_deleted = soft_deleted
self.options = options
self.search_cols = search_cols
self.engine = engine(self)
@staticmethod
def mongo(query_class,
model,
inputs,
mode: WorkingMode = WorkingMode.Development,
extra_filter=None,
soft_deleted=False,
default_sort_by=None, joins=None, search_cols=[]):
return NewTableEngine(query_class, model, inputs, mode, engine=MongoEngine, extra_filter=extra_filter,
default_sort_by=default_sort_by, joins=joins, search_cols=search_cols,
soft_deleted=soft_deleted)
@staticmethod
def mssql(query_class, model, inputs, mode: WorkingMode = WorkingMode.Development, extra_filter=None,
default_sort_by=None, joins=None, search_cols=[], options=None):
return NewTableEngine(query_class, model, inputs, mode, engine=MsSqlEngine, extra_filter=extra_filter,
default_sort_by=default_sort_by, joins=joins, search_cols=search_cols, options=options)
@staticmethod
def pgsql(query_class, model, inputs, mode: WorkingMode = WorkingMode.Development, extra_filter=None,
default_sort_by=None, joins=None, search_cols=None, options=None):
return NewTableEngine(query_class, model, inputs, mode, engine=PgSqlEngine, extra_filter=extra_filter,
default_sort_by=default_sort_by, joins=joins, search_cols=search_cols, options=options)
async def exe(self):
return await self.engine.exe()
async def to_dict(self) -> TableEngineExport:
rows, count = await self.exe()
self.input.extra_filter = None
del self.engine
return TableEngineExport(
rows=rows,
page=self.input.virtual_page(),
total=count,
te=self.input
)
def add_extra_filter(self, filters):
self.input = filters
return self
def joins(self, joins: List[JoinModel]):
self.joins = joins
return self
def set_custom_entities(self, entities=None):
self.engine.custom_entities = entities
return self
+51
View File
@@ -0,0 +1,51 @@
import abc
__all__ = "AbstractEngine"
class AbstractEngineClass(abc.ABC):
def __init__(self, table_engine):
self.model = table_engine.model
self.query_class = table_engine.query_class
self.input = table_engine.input
self.filter = table_engine.input.f
self.default_sort_by = table_engine.default_sort_by
self.custom_entities = []
self.joins = table_engine.joins
self.options = table_engine.options
self.search_cols = table_engine.search_cols
self.soft_deleted = table_engine.soft_deleted
@abc.abstractmethod
def exe(self):
pass
@abc.abstractmethod
def set_filters(self):
pass
@abc.abstractmethod
def set_sort(self):
pass
@staticmethod
@abc.abstractmethod
def get_expression_filter(col=None, op: str = 'eq', val: str = '') -> dict:
pass
@staticmethod
@abc.abstractmethod
def get_operator(op: str):
pass
def count(self):
pass
@staticmethod
@abc.abstractmethod
def generate_scope_filter(filters: list, general=False):
pass
@abc.abstractmethod
def set_joins(self):
pass
+187
View File
@@ -0,0 +1,187 @@
import datetime
from typing import Union, List
from uuid import UUID
from .AbstractEngine import AbstractEngineClass
from ..utils.Models import FilterItem, FilterScope
class MongoEngine(AbstractEngineClass):
rows = []
stack = []
filters = {}
async def exe(self):
_f = self.filters = self.set_filters()
tmp = self.query_class.find(_f).sort(**self.set_sort())
self.rows = tmp.limit(
self.input.get_limit()).skip(self.input.skip())
return [self.model(**document) async for document in self.rows], await self.count()
def query_generator(self):
if not self.input.has_filter():
return []
filters = self.input.filters()
query = self.generate_scope_filter(filters, general=True)
return query
def set_filters(self):
filters = []
query = self.query_generator()
if query:
filters.append(query)
if self.input.has_text_search():
items = []
_search_cols = self.input.get_search_cols() if (
self.input.get_search_cols() and len(self.input.get_search_cols())) else self.search_cols
if _search_cols:
for col in _search_cols:
items.append({col: {'$regex': self.input.get_text_search()}})
filters.append({"$or": items})
if self.input.extra_filter:
filters.extend(self.input.extra_filter)
if self.soft_deleted:
filters.append({'deleted_at': {'$exists': False}})
if not filters:
return {}
self.filters = {'$and': filters}
return self.filters
def set_sort(self):
if self.input.o.field:
return {'key_or_list': self.input.o.field,
'direction': 1 if self.input.o.direction == "asc" else -1}
elif self.default_sort_by:
return self.default_sort_by
else:
return {'key_or_list': 'created_at', 'direction': -1}
@staticmethod
def filter_item_generator(item: FilterItem) -> dict:
if MongoEngine.is_valid_uuid(str(item.col)):
return {
'data': {
"$elemMatch": {
'field_id': UUID(item.col),
'value': {
**MongoEngine.get_expression_filter(item.op, MongoEngine.cast_value(item.val, item.type), )}
}
}
}
return {item.col: {
**MongoEngine.get_expression_filter(col=item.col, op=item.op,
val=MongoEngine.cast_value(item.val, item.type), )}}
@staticmethod
def is_valid_uuid(uuid_to_test, version=4):
try:
uuid_obj = UUID(str(uuid_to_test), version=version)
except ValueError:
return False
return str(uuid_obj) == str(uuid_to_test)
@staticmethod
def cast_value(val: Union[str, List], type: str) -> any:
if type in ['NUMBER', 'STACK', 'FLOAT']:
if isinstance(val, List):
return [float(x) for x in val]
return float(val)
if type in ['DATE', 'DATETIME']:
if isinstance(val, List):
return [datetime.datetime.fromisoformat(str(x)) for x in val]
return datetime.datetime.fromisoformat(val)
if type == 'UUID':
if isinstance(val, List):
return [UUID(str(x)) for x in val]
return UUID(str(val))
return val
@staticmethod
def get_expression_filter(col=None, op: str = 'eq', val: str = '') -> dict:
_op = MongoEngine.get_operator(op)
if _op == "sw":
return {'$regex': f'^{val}'}
elif _op == "ew":
return {'$regex': f'{val}$'}
elif _op == "n_ew":
return {'$regex': f'(?<!{val})$'}
elif _op == "n_sw":
return {'$regex': f'^(?!{val})'}
elif _op == "n_like":
return {'$not': {'$regex': val}}
elif _op == "bet":
if isinstance(val, str):
return {'$gte': val.split('-')[0], '$lte': val.split('-')[1]}
return {'$gte': val[0], '$lte': val[1]}
elif _op == "n_bet":
if isinstance(val, str):
return {'$gte': val.split('-')[0], '$lte': val.split('-')[1]}
return {'$not': {'$gte': val[0], '$lte': val[1]}}
elif _op == "exi":
return {'$exists': True}
elif _op == "n_exi":
return {'$exists': False}
elif _op == "in":
return {'$exists': True}
elif _op == "not_in":
return {'$exists': False}
return {_op: val}
@staticmethod
def get_operator(op: str) -> str:
ops = {
"eq": "$eq",
"n_eq": "$ne",
"gt": "$gt",
"ge": "$ge",
"lt": "$lt",
"le": "$le",
"like": "$regex",
"n_like": "n_like",
"sw": "sw",
"ew": "ew",
'n_ew': 'n_ew',
'n_sw': 'n_sw',
'exi': 'exi',
"n_exi": "n_exi",
'lte': '$lte',
'gte': '$gte',
'in': 'in',
'n_in': 'not_in',
'bet': 'bet',
'n_bet': 'n_bet',
}
return ops[op] if op in ops else '$eq'
async def count(self):
return await self.query_class.count_documents(self.filters)
@staticmethod
def generate_scope_filter(filters: list, general=False):
if len(filters) == 1:
_o = filters[0]
if isinstance(_o, FilterItem):
return MongoEngine.filter_item_generator(filters[0])
elif isinstance(_o, FilterScope):
return MongoEngine.generate_scope_filter(_o.items)
else:
return _o
else:
_a = filters.pop(0)
_a = (MongoEngine.filter_item_generator(_a) if isinstance(_a,
FilterItem) else _a) if not general else (
MongoEngine.generate_scope_filter(_a.items) if isinstance(_a, FilterScope) else _a)
_b = filters.pop(0)
op = '$' + (_b.cr if not general else _b.op)
_b = (MongoEngine.filter_item_generator(_b) if isinstance(_b,
FilterItem) else _b) if not general else (
MongoEngine.generate_scope_filter(_b.items) if isinstance(_b, FilterScope) else _b)
filters = [{op: [
_a, _b
]}] + filters
return MongoEngine.generate_scope_filter(filters, general)
def set_joins(self):
pass
+146
View File
@@ -0,0 +1,146 @@
from sqlalchemy import or_, and_, desc, asc
from sqlalchemy.sql.elements import BooleanClauseList, BinaryExpression, UnaryExpression
from .AbstractEngine import AbstractEngineClass
from ..utils.Models import FilterItem, FilterScope
class MsSqlEngine(AbstractEngineClass):
def set_joins(self):
pass
filters = {}
rows = []
sort = None
async def exe(self):
_f = self.set_filters()
if not self.custom_entities:
query = self.query_class.query(self.model)
else:
query = self.query_class.query(*self.custom_entities)
if self.joins:
for j in self.joins:
if isinstance(j, list):
query = query.join(*j)
else:
query = query.join(j)
if self.options:
query = query.options(*self.options)
tmp = query.filter(_f).order_by(self.set_sort())
self.rows = tmp.limit(self.input.get_limit()).offset(self.input.skip()).all()
return self.rows, tmp.count(), tmp
def query_generator(self):
if not self.input.has_filter():
return None
filters = self.input.filters()
query = self.generate_scope_filter(filters, general=True)
return query
def set_sort(self):
if self.input.o.field:
self.sort = asc(getattr(self.model, self.input.o.field)) if self.input.o.direction == 'asc' else desc(
getattr(self.model, self.input.o.field))
return self.sort
elif isinstance(self.default_sort_by, UnaryExpression):
return self.default_sort_by
else:
return True
def set_filters(self):
filters = []
query = self.query_generator()
if isinstance(query, BooleanClauseList) or isinstance(query, BinaryExpression):
filters.append(query)
if self.input.has_text_search():
items = []
_search_cols = self.input.get_search_cols() if (
self.input.get_search_cols() and len(self.input.get_search_cols())) else self.search_cols
if _search_cols:
for col in _search_cols:
has_join = len(col.split('.')) > 1
if has_join:
pass
else:
items.append(
getattr(self.model, col).ilike("%{search}%".format(search=self.input.get_text_search())))
filters.append(or_(*items))
if self.input.extra_filter:
for f in self.input.extra_filter:
filters.append(f)
if not filters:
return True
self.filters = and_(*filters)
return self.filters
@staticmethod
def get_expression_filter(col=None, op: str = '', val: str = '') -> dict:
_op = MsSqlEngine.get_operator(op)
return getattr(col, op)(val)
@staticmethod
def get_operator(op: str):
ops = {
"eq": "__eq__",
"n_eq": "__ne__",
"gt": "__gt__",
"gte": "__ge__",
"lte": "__lte__",
"lt": "__lt__",
"like": "ilike",
"n_like": "notlike",
"in": "in_",
"n_in": "not_in",
"bet": "between",
"n_bet": "not_between",
"sw": "startswith",
"ew": "endswith",
'n_ew': 'not_endswith',
'n_sw': 'not_startswith',
'exi': 'exi',
"n_exi": "n_exi",
}
return ops[op] if op in ops else '__eq__'
@staticmethod
def get_and_or(op):
return or_ if op == 'or' else and_
def count(self):
pass
def filter_item_generator(self, item: FilterItem):
has_join = len(item.col.split('.')) > 1
if has_join:
pass
else:
return getattr(getattr(self.model, item.col), self.get_operator(item.op))(item.val)
def generate_scope_filter(self, filters: list, general=False):
if len(filters) == 1:
_o = filters[0]
if isinstance(_o, FilterItem):
return self.filter_item_generator(filters[0])
elif isinstance(_o, FilterScope):
return self.generate_scope_filter(_o.items)
else:
return _o
else:
_a = filters.pop(0)
_a = (self.filter_item_generator(_a) if isinstance(_a,
FilterItem) else _a) if not general else (
self.generate_scope_filter(_a.items) if isinstance(_a, FilterScope) else _a)
_b = filters.pop(0)
_op = MsSqlEngine.get_and_or(_b.cr if not general else _b.op)
_b = (self.filter_item_generator(_b) if isinstance(_b,
FilterItem) else _b) if not general else (
self.generate_scope_filter(_b.items) if isinstance(_b, FilterScope) else _b)
filters = [_op(_a, _b)] + filters
return self.generate_scope_filter(filters, general)
+13
View File
@@ -0,0 +1,13 @@
from .AbstractEngine import AbstractEngineClass
class PgSqlEngine(AbstractEngineClass):
def exe(self):
pass
def get_filters(self):
pass
def pagination(self):
pass
+4
View File
@@ -0,0 +1,4 @@
from enum import Enum
from .Mssql import MsSqlEngine
from .Pgsql import PgSqlEngine
from .Mongo import MongoEngine
+19
View File
@@ -0,0 +1,19 @@
import json
from sqlalchemy.orm import DeclarativeMeta
class AlchemyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj.__class__, DeclarativeMeta):
fields = {}
for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
data = obj.__getattribute__(field)
try:
json.dumps(data)
fields[field] = data
except TypeError:
fields[field] = None
return fields
return json.JSONEncoder.default(self, obj)
+160
View File
@@ -0,0 +1,160 @@
from enum import Enum
from typing import Optional, Any, List
import pydantic
from pydantic import BaseModel
class FilterItem(BaseModel):
col: Optional[str]
cr: Optional[str]
id: Optional[str]
op: Optional[str]
type: Optional[str]
val: Optional[Any]
@staticmethod
def ValidItem(item: dict):
return item.get('col') and \
item.get('id') and \
item.get('op') and \
item.get('type')
@staticmethod
def ValidValue(item: dict):
return len(str(item.get('val', '')).strip()) > 0
class Cr(str, Enum):
OR = 'or'
AND = 'and'
class FilterScope(BaseModel):
id: str
items: Optional[List[FilterItem]]
op: Optional[Cr]
def __init__(self, **kwargs):
super().__init__(**kwargs)
if kwargs.get('items'):
self.items = [
FilterItem(**item) for item in filter(lambda i: FilterItem.ValidItem(i), kwargs.get('items', []))]
self.op = Cr(kwargs.get('op'))
self.id = str(kwargs.get('id'))
@staticmethod
def ValidScope(item: dict):
return item.get('id') and item.get('items') and len(
item.get('items')) > 0 and item.get('op')
@staticmethod
def CleanItems(item):
clone = item.dict()
clone['items'] = list(filter(
lambda i: FilterItem.ValidValue(i), clone.get(
'items', [])))
return FilterScope(**clone)
class Page(BaseModel):
c: int = 1
s: int = 5
class SearchModel(BaseModel):
text: Optional[str]
cols: Optional[List[Any]]
class SortModel(BaseModel):
field: Optional[str]
direction: Optional[str]
class JoinModel(BaseModel):
model: BaseModel
show: List[str]
search: Optional[List[str]]
class Config:
extra = pydantic.Extra.ignore
any_str_strip_whitespace = True # strip whitespaces from strings
underscore_attrs_are_private = False
arbitrary_types_allowed = True
class TableEngineInput(BaseModel):
f: Optional[List[FilterScope]]
extra_filter: list = []
o: Optional[SortModel]
p: Optional[Page] = Page()
s: Optional[SearchModel]
is_deleted: Optional[bool]
def __init__(self, **kwargs):
super().__init__(**kwargs)
_input = {**{'p': {'c': 1, 's': 5},
'f': [], 's': {}, 'o': {}}, **(kwargs or {})}
self.p = Page(**_input.get('p'))
self.o = SortModel(**_input.get('o'))
if _input.get('f'):
self.f = [FilterScope(**f) for f in
filter(lambda fs: FilterScope.ValidScope(fs), _input.get('f'))]
self.f = list(map(lambda fs: FilterScope.CleanItems(fs), self.f))
if _input.get('s'):
self.s = SearchModel(**_input.get('s'))
def limit(self):
return self.p.s
def order(self):
return self.o
def filters(self) -> List[FilterScope]:
return self.f
def has_filter(self) -> bool:
return self.f is not None and \
len(self.f) > 0 and \
self.f[0].items is not None and \
len(self.f[0].items) > 0
def has_extra_filter(self) -> bool:
return self.extra_filter is not None and \
len(self.extra_filter) > 0
def skip(self):
return self.page() * self.p.s
def page(self):
return self.p.c - 1
def virtual_page(self):
return self.page() + 1
def get_limit(self):
limit = self.p.s if self.p else 20
return limit if limit < 100 else 100
def text_search_cols(self, cols: List[str]):
if self.s:
self.s.cols = cols
def has_text_search(self) -> bool:
return self.s is not None and self.s.text is not None and len(self.s.text.strip()) > 0
def add_filter(self, _filter):
self.extra_filter.append(_filter)
def get_search_cols(self) -> List[str]:
return self.s.cols if self.s else []
def get_text_search(self) -> str:
return self.s.text.strip() if self.s else None
class WorkingMode(Enum):
Development = "development"
Production = "production"
+150
View File
@@ -0,0 +1,150 @@
import re
from datetime import datetime
from functools import reduce
from time import time
from uuid import uuid4, UUID
import pytz as pytz
import shortuuid
from fastapi.requests import Request
def get_time() -> datetime:
"""
Returns the current time.
Returns:
"""
return datetime.fromtimestamp(time(), tz=pytz.timezone('Asia/Tehran'))
def get_uuid() -> UUID:
"""Returns an unique UUID (UUID4)"""
return uuid4()
def is_numeric(_input) -> bool:
"""Returns True if the input is a number."""
try:
val = int(_input)
return True
except ValueError:
return False
def get_v_id(prefix: str, prefix_len=2, source_id: UUID = None) -> str:
main_id = source_id if source_id is not None else uuid4()
short_id = shortuuid.encode(main_id)
return str(prefix)[0:prefix_len] + '.' + short_id[:7]
def get_qr_v_id(source_id: UUID = None) -> str:
main_id = source_id if source_id is not None else uuid4()
short_id = shortuuid.encode(main_id)
return short_id[:8]
def is_valid_uuid(uuid_to_test, version=4):
"""
Check if uuid_to_test is a valid UUID.
Parameters
----------
uuid_to_test : str
version : {1, 2, 3, 4}
Returns
-------
`True` if uuid_to_test is a valid UUID, otherwise `False`.
Examples
--------
>>> is_valid_uuid('c9bf9e57-1685-4c89-bafb-ff5af830be8a')
True
>>> is_valid_uuid('c9bf9e58')
False
"""
try:
uuid_obj = UUID(str(uuid_to_test), version=version)
except ValueError:
return False
return str(uuid_obj) == str(uuid_to_test)
def get_base_url(request: Request) -> str:
"""
Returns the base url of the request.
Parameters
----------
request : Request
Returns
-------
str
"""
return '{0}://{1}{2}/'.format(request.url.scheme,
request.url.hostname,
':' + request.url.port.__str__() if request.url.port else '')
def p2e_numbers(input: str) -> str:
"""
convert persian numbers to english numbers.
"""
_map = {
'۱': '1',
'۲': '2',
'۳': '3',
'۴': '4',
'۵': '5',
'۶': '6',
'۷': '7',
'۸': '8',
'۹': '9',
'۰': '0'
}
a = map(lambda ch: _map[ch] if ch in _map else ch, input)
return ''.join(list(a))
def is_list_of_strings(lst):
return bool(lst) and isinstance(lst, list) and all(isinstance(elem, str) for elem in lst)
def smart_cast(_input):
if is_numeric(_input):
return int(_input)
if is_valid_uuid(_input):
return UUID(str(_input))
return _input
def is_mobile_number(mobile: str):
regex = r"^\+?[0-0][0-9]{10}$"
if mobile and not re.search(regex, mobile, re.I):
return False
return True
def is_email(email: str):
regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
if email and not re.search(regex, email, re.I):
return False
return True
def deep_get(dictionary, keys, default=None):
return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
def get_tag_strips(tag_name: str, content: str):
try:
r_string = fr'\[{tag_name}\](.|\n)*?\[\/{tag_name}\]'
matches = re.search(r_string, str(content), re.MULTILINE)
raw = matches.group(0)
raw = raw.replace(f'[{tag_name}]', '')
raw = raw.replace(f'[/{tag_name}]', '')
return raw
except BaseException:
return False
+27
View File
@@ -0,0 +1,27 @@
import os
from pathlib import Path
import uvicorn
from config import conf
from core.app import app
"""
This is the main entry point for the application.
"""
# import dynamic middleware in the modules
for i in conf.dependency.middleware:
__import__(f'{i}.middleware')
# import dynamic routes in the modules
for i in conf.dependency.routes:
__import__(f'{i}.urls')
if os.path.exists(Path(f'{i}/hooks')):
__import__(f'{i}.hooks')
# conf.load_modules_config()
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=conf.server.port)

Some files were not shown because too many files have changed in this diff Show More