refs #0 The root commit.
This commit is contained in:
9
.env
Normal file
9
.env
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Маппинг на юзера, под которым все файлы, замонтированные внутрь
|
||||||
|
UID=1000
|
||||||
|
GID=1000
|
||||||
|
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
|
||||||
|
POSTGRES_DATABASE_URL=postgresql://postgres:postgres@db/postgres
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.npmrc
|
||||||
|
.svelte-kit/
|
||||||
|
.idea/
|
||||||
|
node_modules/
|
||||||
|
__pycache__/
|
||||||
|
/postgres
|
||||||
|
tags
|
||||||
|
back/media
|
||||||
|
package-lock.json
|
||||||
6
Dockerfile
Normal file
6
Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.11.4
|
||||||
|
WORKDIR /code
|
||||||
|
COPY ./requirements.txt /code/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||||
|
COPY ./back/core /code/core
|
||||||
|
CMD ["uvicorn", "core.main:app", "--host", "0.0.0.0", "--reload"]
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
110
back/alembic.ini
Normal file
110
back/alembic.ini
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||||
|
# Uncomment the line below if you want the files to be prepended with date and time
|
||||||
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||||
|
# for all available tokens
|
||||||
|
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the python-dateutil library that can be
|
||||||
|
# installed by adding `alembic[tz]` to the pip requirements
|
||||||
|
# string value is passed to dateutil.tz.gettz()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the
|
||||||
|
# "slug" field
|
||||||
|
# truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; This defaults
|
||||||
|
# to alembic/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path.
|
||||||
|
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||||
|
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||||
|
|
||||||
|
# version path separator; As mentioned above, this is the character used to split
|
||||||
|
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||||
|
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||||
|
# Valid values for version_path_separator are:
|
||||||
|
#
|
||||||
|
# version_path_separator = :
|
||||||
|
# version_path_separator = ;
|
||||||
|
# version_path_separator = space
|
||||||
|
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||||
|
|
||||||
|
# set to 'true' to search source files recursively
|
||||||
|
# in each "version_locations" directory
|
||||||
|
# new in Alembic version 1.10
|
||||||
|
# recursive_version_locations = false
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
sqlalchemy.url = postgresql://postgres:postgres@db/postgres
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
# on newly generated revision scripts. See the documentation for further
|
||||||
|
# detail and examples
|
||||||
|
|
||||||
|
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||||
|
# hooks = black
|
||||||
|
# black.type = console_scripts
|
||||||
|
# black.entrypoint = black
|
||||||
|
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
1
back/alembic/README
Normal file
1
back/alembic/README
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
76
back/alembic/env.py
Normal file
76
back/alembic/env.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides
|
||||||
|
# access to the values within the .ini file in use.
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
# This line sets up loggers basically.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
from core.main import Base
|
||||||
|
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
# other values from the config, defined by the needs of env.py,
|
||||||
|
# can be acquired:
|
||||||
|
# my_important_option = config.get_main_option("my_important_option")
|
||||||
|
# ... etc.
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
"""Run migrations in 'offline' mode.
|
||||||
|
|
||||||
|
This configures the context with just a URL
|
||||||
|
and not an Engine, though an Engine is acceptable
|
||||||
|
here as well. By skipping the Engine creation
|
||||||
|
we don't even need a DBAPI to be available.
|
||||||
|
|
||||||
|
Calls to context.execute() here emit the given string to the
|
||||||
|
script output.
|
||||||
|
|
||||||
|
"""
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section, {}),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
24
back/alembic/script.py.mako
Normal file
24
back/alembic/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = ${repr(up_revision)}
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
28
back/alembic/versions/2023_09_13_1727-ceaf19d25957_.py
Normal file
28
back/alembic/versions/2023_09_13_1727-ceaf19d25957_.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: ceaf19d25957
|
||||||
|
Revises:
|
||||||
|
Create Date: 2023-09-13 17:27:54.299295
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'ceaf19d25957'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
0
back/config/__init__.py
Normal file
0
back/config/__init__.py
Normal file
19
back/config/create_schema.py
Normal file
19
back/config/create_schema.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from core.config_factory import CreateSchemaConfig
|
||||||
|
from core.typing import CreateSchema
|
||||||
|
from pydantic import create_model
|
||||||
|
|
||||||
|
|
||||||
|
def create_create_schema(module_name: str) -> CreateSchema:
|
||||||
|
"""
|
||||||
|
Creates pydantic module for item creating.
|
||||||
|
|
||||||
|
Returns pydantic model.
|
||||||
|
|
||||||
|
:param module_name: The name of the module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = CreateSchemaConfig(module_name)
|
||||||
|
|
||||||
|
return create_model(
|
||||||
|
config.module.get_name("create_schema"), **config.get_create_field_defs()
|
||||||
|
)
|
||||||
17
back/config/model.py
Normal file
17
back/config/model.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from core.config_factory import ModelConfig
|
||||||
|
from core.database import Base
|
||||||
|
from core.typing import Model
|
||||||
|
|
||||||
|
|
||||||
|
def create_model(module_name: str) -> Model:
|
||||||
|
"""
|
||||||
|
Creates SQLAlchemy model.
|
||||||
|
|
||||||
|
Returns created SQLAlchemy model.
|
||||||
|
|
||||||
|
:param module_name: The name of the module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = ModelConfig(module_name)
|
||||||
|
|
||||||
|
return type(config.module.get_name("model"), (Base,), config.get_model_attrs())
|
||||||
60
back/config/module/__init__.py
Normal file
60
back/config/module/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from importlib import import_module
|
||||||
|
from os import listdir
|
||||||
|
from os.path import isdir, join
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from core.helper import snake_to_camel
|
||||||
|
from yaml import safe_load
|
||||||
|
|
||||||
|
exclude = "__pycache__"
|
||||||
|
|
||||||
|
PATH = join(*__name__.split("."))
|
||||||
|
MODULES = tuple(
|
||||||
|
filter(
|
||||||
|
lambda item: isdir(join(PATH, item)) and item not in exclude,
|
||||||
|
listdir(PATH),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Module:
|
||||||
|
"""
|
||||||
|
Represents a module of application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, title: str) -> None:
|
||||||
|
self.title = title
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config_path(self) -> str:
|
||||||
|
return f"config/module/{self.title}/config.yaml"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def create_schema_name(self) -> str:
|
||||||
|
return snake_to_camel(self.title) + "Create"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_name(self) -> str:
|
||||||
|
return snake_to_camel(self.title)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schema_name(self) -> str:
|
||||||
|
return self.model_name
|
||||||
|
|
||||||
|
def get_name(self, _type: Literal["model", "schema", "create_schema"]) -> str:
|
||||||
|
match _type:
|
||||||
|
case "model" | "schema":
|
||||||
|
name = snake_to_camel(self.title)
|
||||||
|
case "create_schema":
|
||||||
|
name = snake_to_camel(self.title) + "Create"
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
def get_item(self, _type: Literal["model", "schema", "create_schema"]):
|
||||||
|
return getattr(import_module(f"config.{_type}"), f"create_{_type}")(self.title)
|
||||||
|
|
||||||
|
def get_body(self) -> dict:
|
||||||
|
with open(self.config_path, "r") as stream:
|
||||||
|
project = safe_load(stream)
|
||||||
|
|
||||||
|
return project
|
||||||
0
back/config/module/book/__init__.py
Normal file
0
back/config/module/book/__init__.py
Normal file
9
back/config/module/book/config.yaml
Normal file
9
back/config/module/book/config.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
fields:
|
||||||
|
author:
|
||||||
|
type: str
|
||||||
|
title:
|
||||||
|
type: str
|
||||||
|
year:
|
||||||
|
type: int
|
||||||
|
pdf:
|
||||||
|
type: file
|
||||||
3
back/config/module/book/create_schema.py
Normal file
3
back/config/module/book/create_schema.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from core.util import update_module_locals
|
||||||
|
|
||||||
|
update_module_locals(__name__, locals())
|
||||||
3
back/config/module/book/model.py
Normal file
3
back/config/module/book/model.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from core.util import update_module_locals
|
||||||
|
|
||||||
|
update_module_locals(__name__, locals())
|
||||||
3
back/config/module/book/schema.py
Normal file
3
back/config/module/book/schema.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from core.util import update_module_locals
|
||||||
|
|
||||||
|
update_module_locals(__name__, locals())
|
||||||
21
back/config/schema.py
Normal file
21
back/config/schema.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from core.config_factory import SchemaConfig
|
||||||
|
from core.typing import Schema
|
||||||
|
from pydantic import create_model
|
||||||
|
|
||||||
|
|
||||||
|
def create_schema(module_name: str) -> Schema:
|
||||||
|
"""
|
||||||
|
Creates pydantic module for item returning.
|
||||||
|
|
||||||
|
Returns pydantic model.
|
||||||
|
|
||||||
|
:param module_name: The name of the module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = SchemaConfig(module_name)
|
||||||
|
|
||||||
|
return create_model(
|
||||||
|
config.module.get_name("schema"),
|
||||||
|
__config__=config.Config,
|
||||||
|
**config.get_field_defs(),
|
||||||
|
)
|
||||||
0
back/core/__init__.py
Normal file
0
back/core/__init__.py
Normal file
35
back/core/command.py
Normal file
35
back/core/command.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from os import mkdir
|
||||||
|
|
||||||
|
|
||||||
|
def create_project(name: str) -> None:
|
||||||
|
"""
|
||||||
|
Creates project folder and all required files.
|
||||||
|
|
||||||
|
from core import command; command.create_project("project")
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The desired project name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _create_file(
|
||||||
|
file_name: str, # Literal["__init__", "config", "create_schema", "model", "schema"],
|
||||||
|
content: str = None, # Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
file = open("config/module/" + name + "/" + file_name, "w")
|
||||||
|
if content:
|
||||||
|
file.write(content)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
mkdir("config/module/" + name)
|
||||||
|
|
||||||
|
files_without_content = ("__init__.py", "config.yaml")
|
||||||
|
|
||||||
|
files_with_content = ("create_schema.py", "model.py", "schema.py")
|
||||||
|
|
||||||
|
content = "from core.util import update_module_locals\n\nupdate_module_locals(__name__, locals())\n"
|
||||||
|
|
||||||
|
for file_name in files_without_content:
|
||||||
|
_create_file(file_name)
|
||||||
|
|
||||||
|
for file_name in files_with_content:
|
||||||
|
_create_file(file_name, content)
|
||||||
99
back/core/config_factory.py
Normal file
99
back/core/config_factory.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
from config.module import Module
|
||||||
|
from core.field import get_field
|
||||||
|
from core.pydantic_field import get_pydantic_field
|
||||||
|
from sqlalchemy import Column, Integer
|
||||||
|
from yaml import safe_load
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""
|
||||||
|
Base class for ModelConfig, SchemaConfig and CreateSchemaConfig.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, module_name: str) -> None:
|
||||||
|
"""
|
||||||
|
Initialize configuration.
|
||||||
|
|
||||||
|
:param module_name: The name of module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.module = Module(module_name)
|
||||||
|
self.project = self.get_project()
|
||||||
|
|
||||||
|
def get_project(self) -> dict:
|
||||||
|
"""
|
||||||
|
Construct and return dictionary with project attributes from yaml file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(self.module.config_path, "r") as stream:
|
||||||
|
project = safe_load(stream)
|
||||||
|
|
||||||
|
return project
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class ModelConfig(Config):
|
||||||
|
"""
|
||||||
|
Configurates sqlalchemy model of module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_model_attrs(self) -> dict:
|
||||||
|
"""
|
||||||
|
Returns model configuration for the module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file_fields = set()
|
||||||
|
|
||||||
|
model_attrs = {
|
||||||
|
"__tablename__": self.module.title,
|
||||||
|
"id": Column(Integer, primary_key=True, index=True),
|
||||||
|
}
|
||||||
|
for field_name, field_attrs in self.project.get("fields").items():
|
||||||
|
model_attrs[field_name] = get_field(field_attrs)
|
||||||
|
|
||||||
|
if field_attrs["type"] == "file":
|
||||||
|
file_fields.add(field_name)
|
||||||
|
|
||||||
|
model_attrs["_file_fields"] = file_fields
|
||||||
|
|
||||||
|
return model_attrs
|
||||||
|
|
||||||
|
|
||||||
|
class CreateSchemaConfig(Config):
|
||||||
|
"""
|
||||||
|
Configurates pydantic model for creating items of module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_create_field_defs(self):
|
||||||
|
"""
|
||||||
|
Returns all pydantic model fields.
|
||||||
|
"""
|
||||||
|
|
||||||
|
create_field_definitions = {
|
||||||
|
field_name: get_pydantic_field(field_attrs)
|
||||||
|
for field_name, field_attrs in self.project.get("fields").items()
|
||||||
|
if field_attrs["type"] != "file" and get_pydantic_field(field_attrs)
|
||||||
|
}
|
||||||
|
return create_field_definitions
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaConfig(Config):
|
||||||
|
"""
|
||||||
|
Configurates pydantic model for updating and returning items of module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_field_defs(self):
|
||||||
|
"""
|
||||||
|
Return all pydantic model fields and ID field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field_definitions = {}
|
||||||
|
field_definitions["id"] = (int, ...)
|
||||||
|
|
||||||
|
for field_name, field_attrs in self.project.get("fields").items():
|
||||||
|
if get_pydantic_field(field_attrs):
|
||||||
|
field_definitions[field_name] = get_pydantic_field(field_attrs)
|
||||||
|
|
||||||
|
return field_definitions
|
||||||
68
back/core/crud.py
Normal file
68
back/core/crud.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
|
||||||
|
def add_item(db: Session, cls, item):
|
||||||
|
db_item = cls(**item.dict())
|
||||||
|
db.add(db_item)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_item)
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
|
||||||
|
def view_item(db: Session, cls, item_id: int):
|
||||||
|
return db.query(cls).filter(cls.id == item_id).first()
|
||||||
|
|
||||||
|
|
||||||
|
def edit_item(db: Session, cls, item_id: int, item):
|
||||||
|
db_item = db.query(cls).filter(cls.id == item_id).first()
|
||||||
|
for key, value in item.dict().items():
|
||||||
|
setattr(db_item, key, value)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_item)
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
|
||||||
|
def remove_item(db: Session, cls, item_id: int):
|
||||||
|
db_item = db.query(cls).filter(cls.id == item_id).first()
|
||||||
|
db.delete(db_item)
|
||||||
|
db.commit()
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
|
||||||
|
def view_items(db: Session, cls, skip: int = 0, limit: int = 100):
|
||||||
|
return db.query(cls).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
|
||||||
|
def add_file(db: Session, cls, item_id: int, field: str, file):
|
||||||
|
content = file.file.read()
|
||||||
|
|
||||||
|
if content:
|
||||||
|
file_path = os.path.join("media", file.filename)
|
||||||
|
else:
|
||||||
|
file_path = None
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
db_item = db.query(cls).filter(cls.id == item_id).first()
|
||||||
|
setattr(db_item, field, file_path)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_item)
|
||||||
|
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
|
||||||
|
def remove_file(db: Session, cls, item_id: int, field: str):
|
||||||
|
db_item = db.query(cls).filter(cls.id == item_id).first()
|
||||||
|
file_path = getattr(db_item, field)
|
||||||
|
if file_path:
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
setattr(db_item, field, None)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_item)
|
||||||
|
|
||||||
|
return db_item
|
||||||
13
back/core/database.py
Normal file
13
back/core/database.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URL = os.getenv("POSTGRES_DATABASE_URL")
|
||||||
|
|
||||||
|
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
0
back/core/exception.py
Normal file
0
back/core/exception.py
Normal file
27
back/core/field.py
Normal file
27
back/core/field.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String
|
||||||
|
|
||||||
|
|
||||||
|
def get_field(field_attrs: dict) -> Column:
|
||||||
|
"""
|
||||||
|
Construct sqlalchemy field using dictionary with field attributes.
|
||||||
|
|
||||||
|
Returns the sqlalchemy column.
|
||||||
|
|
||||||
|
:param field_attrs: The dictionary with field attributes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
column_attrs = {}
|
||||||
|
for key, value in field_attrs.items():
|
||||||
|
if key == "type":
|
||||||
|
match value:
|
||||||
|
case "str":
|
||||||
|
column_attrs["type_"] = String
|
||||||
|
case "int":
|
||||||
|
column_attrs["type_"] = Integer
|
||||||
|
case "file":
|
||||||
|
column_attrs["type_"] = String
|
||||||
|
|
||||||
|
elif key == "nullable":
|
||||||
|
column_attrs["nullable"] = value
|
||||||
|
|
||||||
|
return Column(**column_attrs)
|
||||||
18
back/core/helper.py
Normal file
18
back/core/helper.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
def snake_to_camel(snake_string: str) -> str:
|
||||||
|
"""
|
||||||
|
Transform string from snake case to string in camel case.
|
||||||
|
|
||||||
|
Returns the string in tme camel case.
|
||||||
|
|
||||||
|
:param snake_string: The string in the snake case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return "".join(part.capitalize() for part in snake_string.split("_"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_name(name: str) -> str:
|
||||||
|
return name.split(".")[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_parent_directory_name(name: str) -> str:
|
||||||
|
return name.split(".")[-2]
|
||||||
108
back/core/main.py
Normal file
108
back/core/main.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from config.module import MODULES, Module
|
||||||
|
from core import crud
|
||||||
|
from core.database import Base, engine
|
||||||
|
from core.util import get_session
|
||||||
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException, UploadFile
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
def get_router(module: Module) -> APIRouter:
|
||||||
|
cls = module.get_item("model")
|
||||||
|
Item = module.get_item("schema")
|
||||||
|
ItemCreate = module.get_item("create_schema")
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/" + module.title, tags=[module.title])
|
||||||
|
|
||||||
|
@router.post("/", response_model=Item)
|
||||||
|
async def create_item(item: ItemCreate, db: Session = Depends(get_session)):
|
||||||
|
return crud.add_item(db, cls, item)
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[Item])
|
||||||
|
async def list_item(
|
||||||
|
skip: int = 0, limit: int = 100, db: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
items = crud.view_items(db, cls, skip, limit)
|
||||||
|
return items
|
||||||
|
|
||||||
|
@router.get("/{item_id}", response_model=Item)
|
||||||
|
async def read_item(item_id: int, db: Session = Depends(get_session)):
|
||||||
|
db_item = crud.view_item(db, cls, item_id)
|
||||||
|
if db_item is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
@router.put("/{item_id}", response_model=Item)
|
||||||
|
async def update_item(
|
||||||
|
item_id: int,
|
||||||
|
item: ItemCreate,
|
||||||
|
db: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
return crud.edit_item(db, cls, item_id, item)
|
||||||
|
|
||||||
|
@router.delete("/{item_id}", response_model=Item)
|
||||||
|
async def delete_item(item_id: int, db: Session = Depends(get_session)):
|
||||||
|
db_item = crud.view_item(db, cls, item_id)
|
||||||
|
if db_item is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
|
|
||||||
|
for field in db_item._file_fields:
|
||||||
|
crud.remove_file(db, cls, item_id, field)
|
||||||
|
crud.remove_item(db, cls, item_id)
|
||||||
|
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
@router.get("/{item_id}/{field}", response_class=FileResponse)
|
||||||
|
async def read_file(item_id: int, field: str, db: Session = Depends(get_session)):
|
||||||
|
db_item = crud.view_item(db, cls, item_id)
|
||||||
|
return getattr(db_item, field)
|
||||||
|
|
||||||
|
@router.post("/{item_id}/{field}", response_model=Optional[str])
|
||||||
|
async def create_file(
|
||||||
|
item_id: int, field: str, file: UploadFile, db: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
db_item = crud.add_file(db, cls, item_id, field, file)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=500, detail="Can not upload file")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
file.file.close()
|
||||||
|
|
||||||
|
return getattr(db_item, field)
|
||||||
|
|
||||||
|
@router.delete("/{item_id}/{field}", response_model=Item)
|
||||||
|
async def delete_file(item_id: int, field: str, db: Session = Depends(get_session)):
|
||||||
|
db_item = crud.remove_file(db, cls, item_id, field)
|
||||||
|
return db_item
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
for module in tuple(Module(module_name) for module_name in MODULES):
|
||||||
|
app.include_router(get_router(module))
|
||||||
|
|
||||||
|
module_router = APIRouter(prefix="/_module", tags=["_module"])
|
||||||
|
|
||||||
|
|
||||||
|
@module_router.get("/", response_model=list[str])
|
||||||
|
def list_module(skip: int = 0, limit: int = 100):
|
||||||
|
return MODULES
|
||||||
|
|
||||||
|
|
||||||
|
@module_router.get("/{module}", response_model=dict)
|
||||||
|
def read_module(module: str):
|
||||||
|
if module not in MODULES:
|
||||||
|
raise HTTPException(status_code=404, detail="Module not found")
|
||||||
|
return Module(module).get_body()
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(module_router)
|
||||||
|
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
10
back/core/pydantic_field.py
Normal file
10
back/core/pydantic_field.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
def get_pydantic_field(field_attrs: dict) -> tuple:
|
||||||
|
match field_attrs["type"]:
|
||||||
|
case "int" | "str":
|
||||||
|
field = (field_attrs["type"], ...)
|
||||||
|
case "file":
|
||||||
|
field = (str, None)
|
||||||
|
case _:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
return field
|
||||||
75
back/core/test.py
Normal file
75
back/core/test.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from re import match
|
||||||
|
from os.path import exists
|
||||||
|
from unittest import TestCase, main
|
||||||
|
from shutil import rmtree
|
||||||
|
|
||||||
|
from command import create_project
|
||||||
|
|
||||||
|
|
||||||
|
def increment_name(full_name: str) -> str:
|
||||||
|
name, index = match(
|
||||||
|
pattern="^([a-z\d]+)(?:_(\d+))?", string=full_name.lower()
|
||||||
|
).groups()
|
||||||
|
|
||||||
|
if not index:
|
||||||
|
postfix = "_1"
|
||||||
|
else:
|
||||||
|
postfix = f"_{index+1}"
|
||||||
|
|
||||||
|
return name + postfix
|
||||||
|
|
||||||
|
|
||||||
|
def get_temporary_directory_name() -> str:
|
||||||
|
dir_name = "test"
|
||||||
|
while exists("config/module/" + dir_name + "/"):
|
||||||
|
dir_name = increment_name(dir_name)
|
||||||
|
|
||||||
|
return dir_name
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommandMethods(TestCase):
|
||||||
|
def test_create_project_creates_project_directory(self: TestCase):
|
||||||
|
directory = get_temporary_directory_name()
|
||||||
|
create_project(directory)
|
||||||
|
directory_exists = exists(f"config/module/{directory}/")
|
||||||
|
rmtree(f"config/module/{directory}/")
|
||||||
|
self.assertTrue(directory_exists)
|
||||||
|
|
||||||
|
def test_create_project_creates_init(self: TestCase):
|
||||||
|
directory = get_temporary_directory_name()
|
||||||
|
create_project(directory)
|
||||||
|
file_exists = exists(f"config/module/{directory}/__init__.py")
|
||||||
|
rmtree(f"config/module/{directory}/")
|
||||||
|
self.assertTrue(file_exists)
|
||||||
|
|
||||||
|
def test_create_project_creates_config(self: TestCase):
|
||||||
|
directory = get_temporary_directory_name()
|
||||||
|
create_project(directory)
|
||||||
|
file_exists = exists(f"config/module/{directory}/config.yaml")
|
||||||
|
rmtree(f"config/module/{directory}/")
|
||||||
|
self.assertTrue(file_exists)
|
||||||
|
|
||||||
|
def test_create_project_creates_create_schema(self: TestCase):
|
||||||
|
directory = get_temporary_directory_name()
|
||||||
|
create_project(directory)
|
||||||
|
file_exists = exists(f"config/module/{directory}/create_schema.py")
|
||||||
|
rmtree(f"config/module/{directory}/")
|
||||||
|
self.assertTrue(file_exists)
|
||||||
|
|
||||||
|
def test_create_project_creates_model(self: TestCase):
|
||||||
|
directory = get_temporary_directory_name()
|
||||||
|
create_project(directory)
|
||||||
|
file_exists = exists(f"config/module/{directory}/model.py")
|
||||||
|
rmtree(f"config/module/{directory}/")
|
||||||
|
self.assertTrue(file_exists)
|
||||||
|
|
||||||
|
def test_create_project_creates_schema(self: TestCase):
|
||||||
|
directory = get_temporary_directory_name()
|
||||||
|
create_project(directory)
|
||||||
|
file_exists = exists(f"config/module/{directory}/schema.py")
|
||||||
|
rmtree(f"config/module/{directory}/")
|
||||||
|
self.assertTrue(file_exists)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
9
back/core/typing.py
Normal file
9
back/core/typing.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from core.database import Base
|
||||||
|
|
||||||
|
Model = TypeVar("Model", bound=Base)
|
||||||
|
Schema = TypeVar("Schema", bound=BaseModel)
|
||||||
|
CreateSchema = TypeVar("CreateSchema", bound=BaseModel)
|
||||||
24
back/core/util.py
Normal file
24
back/core/util.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from config.module import Module
|
||||||
|
from core.database import SessionLocal
|
||||||
|
from core.helper import get_file_name, get_parent_directory_name
|
||||||
|
|
||||||
|
|
||||||
|
def update_module_locals(path, _locals):
|
||||||
|
item = get_file_name(path)
|
||||||
|
module_name = get_parent_directory_name(path)
|
||||||
|
|
||||||
|
module = Module(module_name)
|
||||||
|
|
||||||
|
_locals[module.get_name(item)] = module.get_item(item)
|
||||||
|
|
||||||
|
|
||||||
|
def get_session():
|
||||||
|
session = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_module(module: str) -> dict:
|
||||||
|
return Module(module).get_item("create_schema").construct().schema()
|
||||||
38
back/help.py
Executable file
38
back/help.py
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from click import argument, command
|
||||||
|
|
||||||
|
|
||||||
|
@command()
|
||||||
|
@argument("path")
|
||||||
|
def pyhelp(path: str):
|
||||||
|
"""
|
||||||
|
Print the object documentation from docstring.
|
||||||
|
|
||||||
|
:param path: The full path to the module, function or class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
module_path = path.rsplit(".", 1)[0]
|
||||||
|
object_name = path.split(".")[-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = import_module(module_path)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
module = None
|
||||||
|
|
||||||
|
if module:
|
||||||
|
if module_path == object_name:
|
||||||
|
_object = module
|
||||||
|
else:
|
||||||
|
_object = getattr(module, object_name, None)
|
||||||
|
else:
|
||||||
|
_object = getattr(__builtins__, object_name, None)
|
||||||
|
|
||||||
|
if _object:
|
||||||
|
help(_object)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pyhelp()
|
||||||
35
back/manage.py
Normal file
35
back/manage.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/local/bin/python
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from click import argument, command
|
||||||
|
|
||||||
|
|
||||||
|
@command()
|
||||||
|
@argument("path")
|
||||||
|
def pyhelp(path: str):
|
||||||
|
"""
|
||||||
|
Вывести документацию к объекту.
|
||||||
|
"""
|
||||||
|
module_path = path.rsplit(".", 1)[0]
|
||||||
|
object_name = path.split(".")[-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = import_module(module_path)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
module = None
|
||||||
|
|
||||||
|
if module:
|
||||||
|
if module_path == object_name:
|
||||||
|
_object = module
|
||||||
|
else:
|
||||||
|
_object = getattr(module, object_name, None)
|
||||||
|
else:
|
||||||
|
_object = getattr(__builtins__, object_name, None)
|
||||||
|
|
||||||
|
if _object:
|
||||||
|
help(_object)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pyhelp()
|
||||||
49
docker-compose.yml
Normal file
49
docker-compose.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
version: "3.3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
shared:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: library-back
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./back:/code
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8000:8000"
|
||||||
|
user: "1000:1000"
|
||||||
|
networks:
|
||||||
|
- shared
|
||||||
|
|
||||||
|
front:
|
||||||
|
image: node:latest
|
||||||
|
container_name: library-front
|
||||||
|
restart: always
|
||||||
|
working_dir: /code
|
||||||
|
volumes:
|
||||||
|
- ./front:/code
|
||||||
|
command: bash -c "npm install && npm run dev -- --host 0.0.0.0"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5173:5173"
|
||||||
|
networks:
|
||||||
|
- shared
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
container_name: library-db
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./postgres:/var/lib/postgresql/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5432:5432"
|
||||||
|
networks:
|
||||||
|
- shared
|
||||||
1
front/.env
Normal file
1
front/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
BACKEND_URL="http://app:8000"
|
||||||
17
front/package.json
Normal file
17
front/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "library",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^2.0.0",
|
||||||
|
"@sveltejs/kit": "^1.5.0",
|
||||||
|
"svelte": "^3.54.0",
|
||||||
|
"vite": "^4.0.0"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
12
front/src/app.html
Normal file
12
front/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
front/src/lib/components/AddForm.svelte
Normal file
26
front/src/lib/components/AddForm.svelte
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script>
|
||||||
|
export let item, project, title;
|
||||||
|
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
import Field from './Field.svelte';
|
||||||
|
import { getKeys, handleCreateItem } from './util.js';
|
||||||
|
|
||||||
|
$: keys = getKeys(item);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" use:enhance>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{#each keys as key}
|
||||||
|
<tr><td>
|
||||||
|
<Field project={project} item={item} key={key}/>
|
||||||
|
</td></tr>
|
||||||
|
{/each}
|
||||||
|
<tr><td>
|
||||||
|
<button on:click|preventDefault={handleCreateItem(title)}>Save</button>
|
||||||
|
</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
35
front/src/lib/components/EditForm.svelte
Normal file
35
front/src/lib/components/EditForm.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script>
|
||||||
|
export let item, project, title;
|
||||||
|
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
import Field from '$lib/components/Field.svelte';
|
||||||
|
import FileField from '$lib/components/FileField.svelte';
|
||||||
|
|
||||||
|
import { getKeys, getType, handleUpdateItem, handleDeleteItem } from './util.js';
|
||||||
|
|
||||||
|
$: keys = getKeys(item);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="POST" enctype="multipart/form-data" use:enhance>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{#each keys as key}
|
||||||
|
<tr><td>
|
||||||
|
{#if getType(project, key) == 'file'}
|
||||||
|
<FileField title={title} item={item} key={key}/>
|
||||||
|
{:else}
|
||||||
|
<Field project={project} item={item} key={key}/>
|
||||||
|
{/if}
|
||||||
|
</td></tr>
|
||||||
|
{/each}
|
||||||
|
<tr><td>
|
||||||
|
<button on:click|preventDefault={handleUpdateItem(title, item.id)}>Save</button>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
<button on:click|preventDefault={handleDeleteItem(title, item.id)}>Delete</button>
|
||||||
|
</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
17
front/src/lib/components/Field.svelte
Normal file
17
front/src/lib/components/Field.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
export let item, key, project;
|
||||||
|
|
||||||
|
import { capitalize, getType, getValue } from './util.js';
|
||||||
|
|
||||||
|
$: type = getType(project, key);
|
||||||
|
$: value = getValue(item, key);
|
||||||
|
$: label = capitalize(key);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label for={key}>{label}:</label>
|
||||||
|
<input
|
||||||
|
name={key}
|
||||||
|
value={value}
|
||||||
|
autocomplete="off"
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
21
front/src/lib/components/FileField.svelte
Normal file
21
front/src/lib/components/FileField.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
export let item, key, title;
|
||||||
|
|
||||||
|
import { capitalize, getFileName, getValue, handleDeleteFile } from './util.js';
|
||||||
|
|
||||||
|
$: label = capitalize(key);
|
||||||
|
$: value = getValue(item, key);
|
||||||
|
$: fileName = getFileName(value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label for={key}>{label}:</label>
|
||||||
|
<input
|
||||||
|
name={key}
|
||||||
|
autocomplete="off"
|
||||||
|
type="file"
|
||||||
|
id={key}
|
||||||
|
/>
|
||||||
|
{#if value}
|
||||||
|
<a href="/{title}/{item.id}/{key}" target="_blank">{fileName}</a>
|
||||||
|
<button on:click|preventDefault={handleDeleteFile(title, item.id, key)}>Delete File</button>
|
||||||
|
{/if}
|
||||||
12
front/src/lib/components/HeadRow.svelte
Normal file
12
front/src/lib/components/HeadRow.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
export let keys;
|
||||||
|
|
||||||
|
import { capitalize } from './util.js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
{#each keys as key}
|
||||||
|
<td>{capitalize(key)}</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
21
front/src/lib/components/Row.svelte
Normal file
21
front/src/lib/components/Row.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
export let title, item, project;
|
||||||
|
|
||||||
|
import { getKeys, getValue, getType, getFileName } from './util.js';
|
||||||
|
|
||||||
|
$: keys = getKeys(item);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><a href="/{title}/{item.id}">{item.id}</a></td>
|
||||||
|
{#each keys as key}
|
||||||
|
<td>
|
||||||
|
{#if getType(project, key) == 'file'}
|
||||||
|
{getFileName(getValue(item, key) || "")}
|
||||||
|
{:else}
|
||||||
|
{getValue(item, key) || ""}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
20
front/src/lib/components/Table.svelte
Normal file
20
front/src/lib/components/Table.svelte
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script>
|
||||||
|
export let items, title, project;
|
||||||
|
|
||||||
|
import HeadRow from './HeadRow.svelte';
|
||||||
|
import Row from './Row.svelte';
|
||||||
|
import { getKeys } from './util.js';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if items.length !== 0}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<HeadRow keys={getKeys(items[0])}/>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each items as item (item.id)}
|
||||||
|
<Row project={project} item={item} title={title}/>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
85
front/src/lib/components/util.js
Normal file
85
front/src/lib/components/util.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { goto, invalidate } from '$app/navigation';
|
||||||
|
|
||||||
|
export function capitalize(str) {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getFileName(str) {
|
||||||
|
if (str !== null) {
|
||||||
|
return str.replace(/^.*[\\\/]/, '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getKeys(item) {
|
||||||
|
return Object.keys(item).filter(key => key !== 'id');
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getValue(item, key) {
|
||||||
|
return item[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTableField(project, item, key) {
|
||||||
|
const value = getValue(item, key) || '';
|
||||||
|
const type = getType(project, key);
|
||||||
|
if (type == 'file') {
|
||||||
|
return getFileName(value);
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getType(project, key) {
|
||||||
|
const type = project.fields[key].type;
|
||||||
|
const type_map = {
|
||||||
|
str: 'text',
|
||||||
|
int: 'number',
|
||||||
|
file: 'file'
|
||||||
|
}
|
||||||
|
return type_map[type]
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function handleCreateItem(project) {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const data = new FormData(form);
|
||||||
|
const response = await fetch(
|
||||||
|
`/${project}/add`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const item = await response.json();
|
||||||
|
goto(`/${project}/${item.id}`, {invalidateAll: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function handleUpdateItem(project, item_id) {
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
const data = new FormData(form);
|
||||||
|
const response = await fetch(
|
||||||
|
`/${project}/${item_id}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
body: data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
invalidate('data:item');
|
||||||
|
invalidate('data:items');
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function handleDeleteItem(project, item_id) {
|
||||||
|
const response = await fetch(
|
||||||
|
`/${project}/${item_id}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
goto(`/${project}`, {invalidateAll: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export async function handleDeleteFile(project, item_id, field) {
|
||||||
|
const response = await fetch(`/${project}/${item_id}/${field}`, {method: 'DELETE'})
|
||||||
|
document.querySelector(`input[name=${field}]`).value = "";
|
||||||
|
invalidate('data:item');
|
||||||
|
invalidate('data:items');
|
||||||
|
};
|
||||||
147
front/src/lib/server/database.js
Normal file
147
front/src/lib/server/database.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { error, fail } from '@sveltejs/kit';
|
||||||
|
import { BACKEND_URL } from '$env/static/private';
|
||||||
|
|
||||||
|
const module = "book"
|
||||||
|
|
||||||
|
export async function getItems(project) {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/`);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getItem(project, id) {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/${id}`);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
const item = await response.json();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEmptyItem(project) {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/_module/${project}`);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
const result = await response.json();
|
||||||
|
const fields = Object.keys(result.fields);
|
||||||
|
const entries = new Map(fields.map(field => [field, null]))
|
||||||
|
return Object.fromEntries(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjects() {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/_module/`);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProject(project) {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/_module/${project}`);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function createItem(project, data) {
|
||||||
|
const fields = Object.fromEntries(
|
||||||
|
Array.from(data.entries()).filter(
|
||||||
|
([key, value]) => !(value instanceof File)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(fields)
|
||||||
|
}
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/`, options);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
|
||||||
|
const item = await response.json();
|
||||||
|
|
||||||
|
const files = Object.fromEntries(
|
||||||
|
Array.from(data.entries()).filter(
|
||||||
|
([key, value]) => value instanceof File && value.size !== 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let key in files)
|
||||||
|
{
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('file', files[key]);
|
||||||
|
await uploadFile(project, item["id"], key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateItem(project, id, data) {
|
||||||
|
const files = Object.fromEntries(
|
||||||
|
Array.from(data.entries()).filter(
|
||||||
|
([key, value]) => value instanceof File && value.size !== 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let key in files)
|
||||||
|
{
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('file', files[key]);
|
||||||
|
await deleteFile(project, id, key);
|
||||||
|
await uploadFile(project, id, key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = Object.fromEntries(
|
||||||
|
Array.from(data.entries()).filter(
|
||||||
|
([key, value]) => !(value instanceof File)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(fields)
|
||||||
|
}
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/${id}`, options);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
|
||||||
|
const item = response.json();
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteItem(project, id) {
|
||||||
|
const options = {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/${id}`, options);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readFile(project, id, field) {
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/${id}/${field}`);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFile(project, id, field, data) {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
}
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/${id}/${field}`, options);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFile(project, id, field) {
|
||||||
|
const options = {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const response = await fetch(`${BACKEND_URL}/${project}/${id}/${field}`, options);
|
||||||
|
if (response.status !== 200) throw error(response.status);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
8
front/src/routes/+layout.server.js
Normal file
8
front/src/routes/+layout.server.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { getProjects } from '$lib/server/database.js';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
const projects = await getProjects();
|
||||||
|
return {
|
||||||
|
projects: projects
|
||||||
|
};
|
||||||
|
}
|
||||||
12
front/src/routes/+layout.svelte
Normal file
12
front/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<a href="/">home</a> |
|
||||||
|
{#each data.projects as project}
|
||||||
|
<a href="/{project}">{project}</a>
|
||||||
|
{/each}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<slot/>
|
||||||
1
front/src/routes/+page.svelte
Normal file
1
front/src/routes/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h1>home</h1>
|
||||||
16
front/src/routes/[project]/+layout.server.js
Normal file
16
front/src/routes/[project]/+layout.server.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import { getItems, getProject, getProjects } from '$lib/server/database.js';
|
||||||
|
|
||||||
|
export async function load({ params, depends }) {
|
||||||
|
depends('data:items');
|
||||||
|
|
||||||
|
const projects = await getProjects();
|
||||||
|
if (!projects.includes(params.project)) throw error(404);
|
||||||
|
const items = await getItems(params.project);
|
||||||
|
const project = await getProject(params.project);
|
||||||
|
return {
|
||||||
|
title: params.project,
|
||||||
|
project: project,
|
||||||
|
items: items
|
||||||
|
};
|
||||||
|
}
|
||||||
7
front/src/routes/[project]/+layout.svelte
Normal file
7
front/src/routes/[project]/+layout.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1>{data.title}</h1>
|
||||||
|
|
||||||
|
<slot/>
|
||||||
12
front/src/routes/[project]/+page.svelte
Normal file
12
front/src/routes/[project]/+page.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import Table from '$lib/components/Table.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Table project={data.project} items={data.items} title={data.title}/>
|
||||||
|
|
||||||
|
<form action="/{data.title}/add" method="GET">
|
||||||
|
<input type="submit" value="Add"/>
|
||||||
|
</form>
|
||||||
0
front/src/routes/[project]/Page.svelte
Normal file
0
front/src/routes/[project]/Page.svelte
Normal file
8
front/src/routes/[project]/[id]/+page.server.js
Normal file
8
front/src/routes/[project]/[id]/+page.server.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { getItem } from '$lib/server/database.js';
|
||||||
|
|
||||||
|
export async function load({ params, depends }) {
|
||||||
|
depends('data:item');
|
||||||
|
|
||||||
|
const item = await getItem(params.project, params.id);
|
||||||
|
return { item };
|
||||||
|
}
|
||||||
7
front/src/routes/[project]/[id]/+page.svelte
Normal file
7
front/src/routes/[project]/[id]/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import EditForm from '$lib/components/EditForm.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<EditForm project={data.project} title={data.title} item={data.item}/>
|
||||||
14
front/src/routes/[project]/[id]/+server.js
Normal file
14
front/src/routes/[project]/[id]/+server.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { deleteItem, updateItem } from '$lib/server/database.js';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function PUT({ params, request }) {
|
||||||
|
const updateData = await request.formData();
|
||||||
|
const item = await updateItem(params.project, params.id, updateData);
|
||||||
|
return json(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE({ params }) {
|
||||||
|
const item = await deleteItem(params.project, params.id);
|
||||||
|
return json(item);
|
||||||
|
}
|
||||||
12
front/src/routes/[project]/[id]/[field]/+server.js
Normal file
12
front/src/routes/[project]/[id]/[field]/+server.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { readFile, deleteFile } from '$lib/server/database.js';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function DELETE({params}) {
|
||||||
|
const response = await deleteFile(params.project, params.id, params.field);
|
||||||
|
return json(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET({params}) {
|
||||||
|
const response = await readFile(params.project, params.id, params.field);
|
||||||
|
return new Response(response.body);
|
||||||
|
}
|
||||||
8
front/src/routes/[project]/add/+page.server.js
Normal file
8
front/src/routes/[project]/add/+page.server.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { getEmptyItem } from '$lib/server/database.js';
|
||||||
|
|
||||||
|
export async function load({ params }) {
|
||||||
|
const item = await getEmptyItem(params.project);
|
||||||
|
|
||||||
|
return { item };
|
||||||
|
}
|
||||||
8
front/src/routes/[project]/add/+page.svelte
Normal file
8
front/src/routes/[project]/add/+page.svelte
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script>
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import AddForm from '$lib/components/AddForm.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AddForm project={data.project} title={data.title} item={data.item}/>
|
||||||
8
front/src/routes/[project]/add/+server.js
Normal file
8
front/src/routes/[project]/add/+server.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createItem } from '$lib/server/database.js';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export async function POST({ params, request }) {
|
||||||
|
const data = await request.formData();
|
||||||
|
const item = await createItem(params.project, data);
|
||||||
|
return json(item);
|
||||||
|
}
|
||||||
BIN
front/static/favicon.png
Normal file
BIN
front/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
10
front/svelte.config.js
Normal file
10
front/svelte.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-auto';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
6
front/vite.config.js
Normal file
6
front/vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [sveltekit()]
|
||||||
|
});
|
||||||
28
nginx.test
Normal file
28
nginx.test
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name library.sbps.ru;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
rewrite ^(.*)$ http://library.sbps.ru$1 permanent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name library.sbps.ru;
|
||||||
|
|
||||||
|
location /lib/media/ {
|
||||||
|
alias /home/www/projects/library_test/media/;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
access_log off;
|
||||||
|
|
||||||
|
include proxy_params;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
proxy_pass http://127.0.0.1:5173;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
requirements.txt
Normal file
53
requirements.txt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
alembic==1.11.1
|
||||||
|
anyio==3.6.2
|
||||||
|
asttokens==2.2.1
|
||||||
|
backcall==0.2.0
|
||||||
|
certifi==2023.5.7
|
||||||
|
click==8.1.3
|
||||||
|
decorator==5.1.1
|
||||||
|
dnspython==2.3.0
|
||||||
|
email-validator==2.0.0.post2
|
||||||
|
executing==1.2.0
|
||||||
|
fastapi==0.95.2
|
||||||
|
greenlet==2.0.2
|
||||||
|
h11==0.14.0
|
||||||
|
httpcore==0.17.1
|
||||||
|
httptools==0.5.0
|
||||||
|
httpx==0.24.1
|
||||||
|
idna==3.4
|
||||||
|
ipython==8.13.2
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
jedi==0.18.2
|
||||||
|
Jinja2==3.1.2
|
||||||
|
Mako==1.2.4
|
||||||
|
MarkupSafe==2.1.2
|
||||||
|
matplotlib-inline==0.1.6
|
||||||
|
orjson==3.8.12
|
||||||
|
parso==0.8.3
|
||||||
|
pexpect==4.8.0
|
||||||
|
pickleshare==0.7.5
|
||||||
|
prompt-toolkit==3.0.38
|
||||||
|
psycopg2-binary==2.9.6
|
||||||
|
ptyprocess==0.7.0
|
||||||
|
pure-eval==0.2.2
|
||||||
|
pydantic==1.10.7
|
||||||
|
Pygments==2.15.1
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
PyYAML==6.0
|
||||||
|
rfc3986==2.0.0
|
||||||
|
six==1.16.0
|
||||||
|
sniffio==1.3.0
|
||||||
|
SQLAlchemy==2.0.15
|
||||||
|
stack-data==0.6.2
|
||||||
|
starlette==0.27.0
|
||||||
|
traitlets==5.9.0
|
||||||
|
typing_extensions==4.5.0
|
||||||
|
ujson==5.7.0
|
||||||
|
uvicorn==0.22.0
|
||||||
|
uvloop==0.17.0
|
||||||
|
watchfiles==0.19.0
|
||||||
|
wcwidth==0.2.6
|
||||||
|
websockets==11.0.3
|
||||||
|
click
|
||||||
|
pysnooper
|
||||||
Reference in New Issue
Block a user