diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fc51622 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,136 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Dandelion +.git +etc/dandelion/dandelion.conf +mypy-report/ +AUTHORS +ChangeLog diff --git a/.gitignore b/.gitignore index b6e4761..ea4801e 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,9 @@ dmypy.json # Pyre type checker .pyre/ + +# Dandelion +etc/dandelion/dandelion.conf +mypy-report/ +AUTHORS +ChangeLog diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..9aef19f --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,9 @@ +[settings] +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 99 +reverse_relative = true +combine_as_imports = true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..59038ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM ubuntu:20.04 + +ARG GIT_BRANCH +ARG GIT_COMMIT +ARG RELEASE_VERSION +ARG REPO_URL + +LABEL dandelion.build_branch=${GIT_BRANCH} \ + dandelion.build_commit=${GIT_COMMIT} \ + dandelion.release_version=${RELEASE_VERSION} \ + dandelion.repo_url=${REPO_URL} + +COPY ./ /dandelion/ +COPY ./etc/dandelion/gunicorn.py /etc/dandelion/gunicorn.py +COPY ./etc/dandelion/dandelion.conf.sample /etc/dandelion/dandelion.conf +COPY ./tools/run_service.sh /usr/local/bin/run_service.sh + +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 + +RUN export LANG=C.UTF-8 \ + && apt-get update -y && apt-get install -y --no-install-recommends apt-utils \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + traceroute lsof iputils-ping vim git wget curl locales-all ssl-cert \ + python3 python3-pip python3-dev python3-venv gcc make \ + && rm -rf /usr/bin/python /usr/bin/pip \ + && ln -s /usr/bin/python3 /usr/bin/python \ + && ln -s /usr/bin/pip3 /usr/bin/pip \ + && cd /dandelion/ \ + && git init \ + && git config --global user.name build \ + && git config --global user.email build@mail.com \ + && git add . \ + && git commit -a -m "Build ${GIT_BRANCH} ${GIT_COMMIT}" \ + && cd / \ + && pip install dandelion/ \ + && apt-get clean \ + && rm -rf ~/.cache/pip + +EXPOSE 28100 + +CMD ["run_service.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6172cc2 --- /dev/null +++ b/Makefile @@ -0,0 +1,110 @@ +SHELL := /bin/bash + +PYTHON ?= python3 +SOURCES := dandelion +TOOLS := tools +ROOT_DIR ?= $(shell git rev-parse --show-toplevel) + +# Color +no_color = \033[0m +black = \033[0;30m +red = \033[0;31m +green = \033[0;32m +yellow = \033[0;33m +blue = \033[0;34m +purple = \033[0;35m +cyan = \033[0;36m +white = \033[0;37m + +# Version +RELEASE_VERSION ?= $(shell git rev-parse --short HEAD)_$(shell date -u +%Y-%m-%dT%H:%M:%S%z) +GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) +GIT_COMMIT ?= $(shell git rev-parse --verify HEAD) + +# Database manage +REV_MEG ?= + +.PHONY: all help venv install fmt lint server clean db_revision db_sync swagger config build future_check + +all: + make venv + make fmt + make lint + make swagger + make config + make future_check + +help: + @echo "Dandelion development makefile" + @echo + @echo "Usage: make " + @echo + @echo "Target:" + @echo " venv Create virtualenvs." + @echo " install Installs the project dependencies." + @echo " fmt Code format." + @echo " lint Code lint." + @echo " server Run Server." + @echo " clean Clean tmp resources." + @echo " db_revision Generate database alembic version revision with model." + @echo " db_sync Sync database from alembic version revision." + @echo " swagger Generate swagger json file." + @echo " config Generate sample config file." + @echo " build Build docker image." + @echo " future_check Find python files without 'type annotations'.(Alpha)" + @echo + +venv: + tox -e venv + +install: + tox -e install + source .tox/install/bin/activate && python setup.py install && deactivate + +fmt: + tox -e pep8-format + +lint: + tox -e pep8 + +server: install + source .tox/install/bin/activate && uvicorn --reload --reload-dir dandelion --port 28300 --log-level debug dandelion.main:app --host 0.0.0.0 + +clean: + rm -rf $(ROOT_DIR)/build + rm -rf $(ROOT_DIR)/dist + rm -rf $(ROOT_DIR)/.venv + rm -rf $(ROOT_DIR)/test_report.html + rm -rf $(ROOT_DIR)/.tox + +db_revision: + $(shell [ -z "$(REV_MEG)" ] && printf '$(red)Missing required message, use "make db_revision REV_MEG="$(no_color)') + source .tox/venv/bin/activate && alembic revision --autogenerate -m '$(REV_MEG)' && deactivate + +db_sync: + source .tox/venv/bin/activate && alembic upgrade head && deactivate + +swagger: + tox -e genswagger + +config: + tox -e genconfig + +BUILD_ENGINE ?= docker +BUILD_CONTEXT ?= . +DOCKER_FILE ?= Dockerfile +IMAGE ?= dandelion +IMAGE_TAG ?= latest +ifeq ($(BUILD_ENGINE), docker) + build_cmd = docker build +else ifeq ($(BUILD_ENGINE), buildah) + build_cmd = buildah bud +else + $(error Unsupported build engine $(BUILD_ENGINE)) +endif +build: + $(build_cmd) --no-cache --pull --force-rm --build-arg RELEASE_VERSION=$(RELEASE_VERSION) --build-arg GIT_BRANCH=$(GIT_BRANCH) --build-arg GIT_COMMIT=$(GIT_COMMIT) $(BUILD_ARGS) -f $(DOCKER_FILE) -t $(IMAGE):$(IMAGE_TAG) $(BUILD_CONTEXT) + +# Find python files without "type annotations" +future_check: + @find dandelion ! -size 0 -type f -name '*.py' -exec grep -L 'from __future__ import annotations' {} \; diff --git a/README.md b/README.md index ffec1af..08c8b6f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,103 @@ # dandelion -Device Management + +OpenV2X Device Management - APIServer + +## Description + +TODO + +## Features + +TODO + +## Installation + +```bash +make install +``` + +## Configuration + +```bash +cp etc/dandelion/dandelion.conf.example etc/dandelion/dandelion.conf +``` + +Generally, you should change the following values: + +[DEFAULT] +- debug +- log_file +- log_dir + +[cors] +- origins + +[database] +- connection + +[mqtt] +- host +- port +- username +- password + +[redis] +- connection + +[token] +- expire_seconds + +```bash +mkdir -p /etc/dandelion +DANDELION_PATH=`pwd` +cd /etc/dandelion +ln -s ${DANDELION_PATH}/etc/dandelion/dandelion.conf dandelion.conf +``` + +## Build && Run + +Build docker image. + +```bash +make build +``` + +Run as container. + +```bash +mkdir -p /var/log/dandelion +docker run -d --name dandelion_bootstrap -e KOLLA_BOOTSTRAP="" -v /etc/dandelion/dandelion.conf:/etc/dandelion/dandelion.conf --net=host dandelion:latest +docker rm dandelion_bootstrap +docker run -d --name dandelion --restart=always -v /etc/dandelion/dandelion.conf:/etc/dandelion/dandelion.conf -v /var/log/dandelion:/var/log/dandelion --net=host dandelion:latest +``` + +## Local Development + +### Run server + +At last, you can run the server. + +```bash +make server +``` + +You can visit the OpenAPI document at `http://127.0.0.1:28300/docs` + +### Genereate the latest swagger file + +```bash +make swagger +``` + +### Generate the latest sample config file + +```bash +make config +``` + +## Code Format && Style + +```bash +make fmt +make lint +``` diff --git a/alembic.ini b/alembic.ini new file mode 100755 index 0000000..e4d1acb --- /dev/null +++ b/alembic.ini @@ -0,0 +1,71 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = dandelion:alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# 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 +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# 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 diff --git a/dandelion/__init__.py b/dandelion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/alembic/README b/dandelion/alembic/README new file mode 100755 index 0000000..98e4f9c --- /dev/null +++ b/dandelion/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/dandelion/alembic/env.py b/dandelion/alembic/env.py new file mode 100755 index 0000000..5d3eeb9 --- /dev/null +++ b/dandelion/alembic/env.py @@ -0,0 +1,100 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations, with_statement + +from logging.config import fileConfig + +from alembic import context +from oslo_config import cfg +from sqlalchemy import create_engine, pool + +import dandelion.conf +from dandelion import constants, version +from dandelion.db.base import Base # noqa + +CONF: cfg = dandelion.conf.CONF + +CONF( + args=["--config-file", constants.CONFIG_FILE_PATH], + project=constants.PROJECT_NAME, + version=version.version_string(), +) + +# 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. +fileConfig(config.config_file_name) # type: ignore + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +# target_metadata = None + + +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(): + """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. + + """ + context.configure( + url=CONF.database.connection, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + engine = create_engine(CONF.database.connection, poolclass=pool.NullPool) + + with engine.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() diff --git a/dandelion/alembic/script.py.mako b/dandelion/alembic/script.py.mako new file mode 100755 index 0000000..2c01563 --- /dev/null +++ b/dandelion/alembic/script.py.mako @@ -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(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/dandelion/alembic/versions/.keep b/dandelion/alembic/versions/.keep new file mode 100755 index 0000000..e69de29 diff --git a/dandelion/alembic/versions/2866653a5623_first_revision.py b/dandelion/alembic/versions/2866653a5623_first_revision.py new file mode 100644 index 0000000..8dad9b3 --- /dev/null +++ b/dandelion/alembic/versions/2866653a5623_first_revision.py @@ -0,0 +1,426 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# flake8: noqa +# fmt: off + +"""first revision + +Revision ID: 2866653a5623 +Revises: +Create Date: 2022-07-05 15:56:30.701441 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '2866653a5623' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('country', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('code', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_country_code'), 'country', ['code'], unique=True) + op.create_index(op.f('ix_country_id'), 'country', ['id'], unique=False) + op.create_table('rsm', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('ref_pos', sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsm_id'), 'rsm', ['id'], unique=False) + op.create_table('rsu_config', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('bsm', sa.JSON(), nullable=True), + sa.Column('rsi', sa.JSON(), nullable=True), + sa.Column('rsm', sa.JSON(), nullable=True), + sa.Column('map', sa.JSON(), nullable=True), + sa.Column('spat', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_config_id'), 'rsu_config', ['id'], unique=False) + op.create_index(op.f('ix_rsu_config_name'), 'rsu_config', ['name'], unique=True) + op.create_table('rsu_log', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('upload_url', sa.String(length=64), nullable=False), + sa.Column('user_id', sa.String(length=64), nullable=False), + sa.Column('password', sa.String(length=255), nullable=False), + sa.Column('transprotocal', sa.Enum('http', 'https', 'ftp', 'sftp', 'other'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_log_id'), 'rsu_log', ['id'], unique=False) + op.create_table('rsu_model', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('manufacturer', sa.String(length=64), nullable=False), + sa.Column('desc', sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_model_id'), 'rsu_model', ['id'], unique=False) + op.create_index(op.f('ix_rsu_model_manufacturer'), 'rsu_model', ['manufacturer'], unique=False) + op.create_index(op.f('ix_rsu_model_name'), 'rsu_model', ['name'], unique=True) + op.create_table('rsu_query', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('query_type', sa.Integer(), nullable=False), + sa.Column('time_type', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_query_id'), 'rsu_query', ['id'], unique=False) + op.create_table('rsu_tmp', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('rsu_id', sa.String(length=64), nullable=False), + sa.Column('rsu_esn', sa.String(length=64), nullable=False), + sa.Column('rsu_name', sa.String(length=64), nullable=False), + sa.Column('rsu_status', sa.String(length=64), nullable=False), + sa.Column('version', sa.String(length=64), nullable=False), + sa.Column('location', sa.JSON(), nullable=False), + sa.Column('config', sa.JSON(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_tmp_id'), 'rsu_tmp', ['id'], unique=False) + op.create_index(op.f('ix_rsu_tmp_rsu_esn'), 'rsu_tmp', ['rsu_esn'], unique=True) + op.create_index(op.f('ix_rsu_tmp_rsu_name'), 'rsu_tmp', ['rsu_name'], unique=False) + op.create_table('user', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('username', sa.String(length=255), nullable=False), + sa.Column('hashed_password', sa.String(length=4096), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_superuser', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False) + op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True) + op.create_table('province', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('country_code', sa.String(length=64), nullable=True), + sa.Column('code', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.ForeignKeyConstraint(['country_code'], ['country.code'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_province_code'), 'province', ['code'], unique=True) + op.create_index(op.f('ix_province_id'), 'province', ['id'], unique=False) + op.create_table('rsm_participants', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('rsm_id', sa.Integer(), nullable=True), + sa.Column('ptc_type', sa.Enum('unknown', 'motor', 'non_motor', 'pedestrian', 'rsu', name='ptctype'), nullable=False), + sa.Column('ptc_id', sa.Integer(), nullable=False), + sa.Column('source', sa.Integer(), nullable=False), + sa.Column('sec_mark', sa.Integer(), nullable=True), + sa.Column('pos', sa.JSON(), nullable=False), + sa.Column('accuracy', sa.String(length=255), nullable=True), + sa.Column('speed', sa.Integer(), nullable=True), + sa.Column('heading', sa.Integer(), nullable=True), + sa.Column('size', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['rsm_id'], ['rsm.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsm_participants_id'), 'rsm_participants', ['id'], unique=False) + op.create_table('city', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('province_code', sa.String(length=64), nullable=True), + sa.Column('code', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.ForeignKeyConstraint(['province_code'], ['province.code'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_city_code'), 'city', ['code'], unique=True) + op.create_index(op.f('ix_city_id'), 'city', ['id'], unique=False) + op.create_table('area', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('city_code', sa.String(length=64), nullable=True), + sa.Column('code', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.ForeignKeyConstraint(['city_code'], ['city.code'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_area_code'), 'area', ['code'], unique=True) + op.create_index(op.f('ix_area_id'), 'area', ['id'], unique=False) + op.create_table('map', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('address', sa.String(length=255), nullable=False, comment='specific location'), + sa.Column('area_code', sa.String(length=64), nullable=True), + sa.Column('desc', sa.String(length=255), nullable=False), + sa.Column('lat', sa.Float(), nullable=False), + sa.Column('lng', sa.Float(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['area_code'], ['area.code'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_map_id'), 'map', ['id'], unique=False) + op.create_index(op.f('ix_map_name'), 'map', ['name'], unique=True) + op.create_table('rsu', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('rsu_id', sa.String(length=64), nullable=False), + sa.Column('rsu_esn', sa.String(length=64), nullable=False, comment='serial number'), + sa.Column('rsu_ip', sa.String(length=64), nullable=False), + sa.Column('rsu_name', sa.String(length=64), nullable=False), + sa.Column('version', sa.String(length=64), nullable=False), + sa.Column('rsu_status', sa.Boolean(), nullable=False), + sa.Column('location', sa.JSON(), nullable=False), + sa.Column('config', sa.JSON(), nullable=False), + sa.Column('online_status', sa.Boolean(), nullable=False), + sa.Column('rsu_model_id', sa.Integer(), nullable=True), + sa.Column('area_code', sa.String(length=64), nullable=True), + sa.Column('address', sa.String(length=255), nullable=False, comment='Installation specific location'), + sa.Column('desc', sa.String(length=255), nullable=True), + sa.Column('log_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['area_code'], ['area.code'], ), + sa.ForeignKeyConstraint(['log_id'], ['rsu_log.id'], ), + sa.ForeignKeyConstraint(['rsu_model_id'], ['rsu_model.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_id'), 'rsu', ['id'], unique=False) + op.create_index(op.f('ix_rsu_online_status'), 'rsu', ['online_status'], unique=False) + op.create_index(op.f('ix_rsu_rsu_esn'), 'rsu', ['rsu_esn'], unique=True) + op.create_index(op.f('ix_rsu_rsu_name'), 'rsu', ['rsu_name'], unique=False) + op.create_index(op.f('ix_rsu_rsu_status'), 'rsu', ['rsu_status'], unique=False) + op.create_table('camera', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('sn', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('stream_url', sa.String(length=255), nullable=False), + sa.Column('lng', sa.Float(), nullable=False), + sa.Column('lat', sa.Float(), nullable=False), + sa.Column('elevation', sa.Float(), nullable=False), + sa.Column('towards', sa.Float(), nullable=False), + sa.Column('status', sa.Boolean(), nullable=False), + sa.Column('rsu_id', sa.Integer(), nullable=True), + sa.Column('desc', sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint(['rsu_id'], ['rsu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_camera_id'), 'camera', ['id'], unique=False) + op.create_index(op.f('ix_camera_name'), 'camera', ['name'], unique=False) + op.create_index(op.f('ix_camera_sn'), 'camera', ['sn'], unique=True) + op.create_table('map_rsu', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('map_id', sa.Integer(), nullable=True), + sa.Column('rsu_id', sa.Integer(), nullable=True), + sa.Column('status', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['map_id'], ['map.id'], ), + sa.ForeignKeyConstraint(['rsu_id'], ['rsu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_map_rsu_id'), 'map_rsu', ['id'], unique=False) + op.create_table('mng', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('rsu_id', sa.Integer(), nullable=True), + sa.Column('heartbeat_rate', sa.Integer(), nullable=True), + sa.Column('running_info_rate', sa.Integer(), nullable=True), + sa.Column('log_rate', sa.Integer(), nullable=True), + sa.Column('log_level', sa.Enum('DEBUG', 'INFO', 'ERROR', 'WARN', 'NOLog'), nullable=True), + sa.Column('reboot', sa.Enum('not_reboot', 'reboot', name='reboot'), nullable=True), + sa.Column('address_change', sa.JSON(), nullable=True), + sa.Column('extend_config', sa.String(length=64), nullable=False), + sa.ForeignKeyConstraint(['rsu_id'], ['rsu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_mng_id'), 'mng', ['id'], unique=False) + op.create_table('radar', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('sn', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=64), nullable=False), + sa.Column('radar_ip', sa.String(length=15), nullable=False), + sa.Column('lng', sa.Float(), nullable=False), + sa.Column('lat', sa.Float(), nullable=False), + sa.Column('elevation', sa.Float(), nullable=False), + sa.Column('towards', sa.Float(), nullable=False), + sa.Column('status', sa.Boolean(), nullable=False), + sa.Column('rsu_id', sa.Integer(), nullable=True), + sa.Column('desc', sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint(['rsu_id'], ['rsu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_radar_id'), 'radar', ['id'], unique=False) + op.create_index(op.f('ix_radar_name'), 'radar', ['name'], unique=False) + op.create_index(op.f('ix_radar_sn'), 'radar', ['sn'], unique=True) + op.create_table('rsi_event', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('rsu_id', sa.Integer(), nullable=True), + sa.Column('area_code', sa.String(length=64), nullable=True), + sa.Column('address', sa.String(length=64), nullable=False), + sa.Column('alert_id', sa.String(length=64), nullable=True), + sa.Column('duration', sa.Integer(), nullable=True), + sa.Column('event_status', sa.Boolean(), nullable=True), + sa.Column('timestamp', sa.String(length=64), nullable=True, comment='yyyy-MM-ddT HH:mm:ss.SSS Z'), + sa.Column('event_class', sa.Enum('AbnormalTraffic', 'AdverseWeather', 'AbnormalVehicle', 'TrafficSign', name='eventclass'), nullable=True), + sa.Column('event_type', sa.Integer(), nullable=True), + sa.Column('event_source', sa.Enum('unknown', 'police', 'government', 'meteorological', 'internet', 'detection', name='eventsource'), nullable=True), + sa.Column('event_confidence', sa.Float(), nullable=True), + sa.Column('event_position', sa.JSON(), nullable=True), + sa.Column('event_radius', sa.Float(), nullable=True), + sa.Column('event_description', sa.String(length=255), nullable=True), + sa.Column('event_priority', sa.Integer(), nullable=True), + sa.Column('reference_paths', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['area_code'], ['area.code'], ), + sa.ForeignKeyConstraint(['rsu_id'], ['rsu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsi_event_address'), 'rsi_event', ['address'], unique=False) + op.create_index(op.f('ix_rsi_event_id'), 'rsi_event', ['id'], unique=False) + op.create_table('rsu_config_rsu', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('rsu_config_id', sa.Integer(), nullable=True), + sa.Column('rsu_id', sa.Integer(), nullable=True), + sa.Column('status', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['rsu_config_id'], ['rsu_config.id'], ), + sa.ForeignKeyConstraint(['rsu_id'], ['rsu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_config_rsu_id'), 'rsu_config_rsu', ['id'], unique=False) + op.create_table('rsu_query_result', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('query_id', sa.Integer(), nullable=True), + sa.Column('rsu_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['query_id'], ['rsu_query.id'], ), + sa.ForeignKeyConstraint(['rsu_id'], ['rsu.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_query_result_id'), 'rsu_query_result', ['id'], unique=False) + op.create_table('rsu_query_result_data', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('create_time', sa.DateTime(), nullable=False), + sa.Column('update_time', sa.DateTime(), nullable=False), + sa.Column('result_id', sa.Integer(), nullable=True), + sa.Column('data', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['result_id'], ['rsu_query_result.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_rsu_query_result_data_id'), 'rsu_query_result_data', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_rsu_query_result_data_id'), table_name='rsu_query_result_data') + op.drop_table('rsu_query_result_data') + op.drop_index(op.f('ix_rsu_query_result_id'), table_name='rsu_query_result') + op.drop_table('rsu_query_result') + op.drop_index(op.f('ix_rsu_config_rsu_id'), table_name='rsu_config_rsu') + op.drop_table('rsu_config_rsu') + op.drop_index(op.f('ix_rsi_event_id'), table_name='rsi_event') + op.drop_index(op.f('ix_rsi_event_address'), table_name='rsi_event') + op.drop_table('rsi_event') + op.drop_index(op.f('ix_radar_sn'), table_name='radar') + op.drop_index(op.f('ix_radar_name'), table_name='radar') + op.drop_index(op.f('ix_radar_id'), table_name='radar') + op.drop_table('radar') + op.drop_index(op.f('ix_mng_id'), table_name='mng') + op.drop_table('mng') + op.drop_index(op.f('ix_map_rsu_id'), table_name='map_rsu') + op.drop_table('map_rsu') + op.drop_index(op.f('ix_camera_sn'), table_name='camera') + op.drop_index(op.f('ix_camera_name'), table_name='camera') + op.drop_index(op.f('ix_camera_id'), table_name='camera') + op.drop_table('camera') + op.drop_index(op.f('ix_rsu_rsu_status'), table_name='rsu') + op.drop_index(op.f('ix_rsu_rsu_name'), table_name='rsu') + op.drop_index(op.f('ix_rsu_rsu_esn'), table_name='rsu') + op.drop_index(op.f('ix_rsu_online_status'), table_name='rsu') + op.drop_index(op.f('ix_rsu_id'), table_name='rsu') + op.drop_table('rsu') + op.drop_index(op.f('ix_map_name'), table_name='map') + op.drop_index(op.f('ix_map_id'), table_name='map') + op.drop_table('map') + op.drop_index(op.f('ix_area_id'), table_name='area') + op.drop_index(op.f('ix_area_code'), table_name='area') + op.drop_table('area') + op.drop_index(op.f('ix_city_id'), table_name='city') + op.drop_index(op.f('ix_city_code'), table_name='city') + op.drop_table('city') + op.drop_index(op.f('ix_rsm_participants_id'), table_name='rsm_participants') + op.drop_table('rsm_participants') + op.drop_index(op.f('ix_province_id'), table_name='province') + op.drop_index(op.f('ix_province_code'), table_name='province') + op.drop_table('province') + op.drop_index(op.f('ix_user_username'), table_name='user') + op.drop_index(op.f('ix_user_id'), table_name='user') + op.drop_table('user') + op.drop_index(op.f('ix_rsu_tmp_rsu_name'), table_name='rsu_tmp') + op.drop_index(op.f('ix_rsu_tmp_rsu_esn'), table_name='rsu_tmp') + op.drop_index(op.f('ix_rsu_tmp_id'), table_name='rsu_tmp') + op.drop_table('rsu_tmp') + op.drop_index(op.f('ix_rsu_query_id'), table_name='rsu_query') + op.drop_table('rsu_query') + op.drop_index(op.f('ix_rsu_model_name'), table_name='rsu_model') + op.drop_index(op.f('ix_rsu_model_manufacturer'), table_name='rsu_model') + op.drop_index(op.f('ix_rsu_model_id'), table_name='rsu_model') + op.drop_table('rsu_model') + op.drop_index(op.f('ix_rsu_log_id'), table_name='rsu_log') + op.drop_table('rsu_log') + op.drop_index(op.f('ix_rsu_config_name'), table_name='rsu_config') + op.drop_index(op.f('ix_rsu_config_id'), table_name='rsu_config') + op.drop_table('rsu_config') + op.drop_index(op.f('ix_rsm_id'), table_name='rsm') + op.drop_table('rsm') + op.drop_index(op.f('ix_country_id'), table_name='country') + op.drop_index(op.f('ix_country_code'), table_name='country') + op.drop_table('country') + # ### end Alembic commands ### diff --git a/dandelion/alembic/versions/__init__.py b/dandelion/alembic/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/api/__init__.py b/dandelion/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/api/api_v1/__init__.py b/dandelion/api/api_v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/api/api_v1/api.py b/dandelion/api/api_v1/api.py new file mode 100644 index 0000000..46b1619 --- /dev/null +++ b/dandelion/api/api_v1/api.py @@ -0,0 +1,77 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from fastapi import APIRouter + +from dandelion.api.api_v1.endpoints import ( + areas, + cameras, + cities, + cloud_homes, + countries, + login, + map_rsus, + maps, + mngs, + provinces, + radars, + rsi_events, + rsm_participants, + rsu_configs, + rsu_logs, + rsu_models, + rsu_queries, + rsu_tmps, + rsus, + users, +) + +api_router = APIRouter() + +api_router.include_router(login.router, prefix="/login", tags=["User"]) +api_router.include_router(users.router, prefix="/users", tags=["User"]) + +api_router.include_router(countries.router, prefix="/countries", tags=["Area"]) +api_router.include_router(provinces.router, prefix="/provinces", tags=["Area"]) +api_router.include_router(cities.router, prefix="/cities", tags=["Area"]) +api_router.include_router(areas.router, prefix="/areas", tags=["Area"]) + +api_router.include_router(cameras.router, prefix="/cameras", tags=["Camera"]) + +api_router.include_router(cloud_homes.router, prefix="/homes", tags=["Cloud Control Home"]) + +api_router.include_router(map_rsus.router, prefix="/maps", tags=["Map"]) +api_router.include_router(maps.router, prefix="/maps", tags=["Map"]) + +api_router.include_router(mngs.router, prefix="/mngs", tags=["MNG"]) + +api_router.include_router(radars.router, prefix="/radars", tags=["Radar"]) + +api_router.include_router(rsi_events.router, prefix="/events", tags=["Event"]) + +api_router.include_router(rsm_participants.router, prefix="/rsms", tags=["RSM"]) + +api_router.include_router(rsu_configs.router, prefix="/rsu_configs", tags=["RSU Config"]) + +api_router.include_router(rsu_logs.router, prefix="/rsu_logs", tags=["RSU Log"]) + +api_router.include_router(rsu_models.router, prefix="/rsu_models", tags=["RSU Model"]) + +api_router.include_router(rsu_queries.router, prefix="/rsu_queries", tags=["RSU Query"]) + +api_router.include_router(rsu_tmps.router, prefix="/rsu_tmps", tags=["RSU TMP"]) + +api_router.include_router(rsus.router, prefix="/rsus", tags=["RSU"]) diff --git a/dandelion/api/api_v1/endpoints/__init__.py b/dandelion/api/api_v1/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/api/api_v1/endpoints/areas.py b/dandelion/api/api_v1/endpoints/areas.py new file mode 100644 index 0000000..3f25a59 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/areas.py @@ -0,0 +1,55 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import List + +from fastapi import APIRouter, Depends, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "", + response_model=List[schemas.Area], + status_code=status.HTTP_200_OK, + description=""" +Search area by city. +""", + responses={ + status.HTTP_200_OK: {"model": List[schemas.Area], "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + city_code: str = Query(..., description="Filter by cityCode", alias="cityCode"), + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> List[schemas.Area]: + areas = crud.area.get_multi_by_city_code(db, city_code) + return [area for area in areas] diff --git a/dandelion/api/api_v1/endpoints/cameras.py b/dandelion/api/api_v1/endpoints/cameras.py new file mode 100644 index 0000000..66ab793 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/cameras.py @@ -0,0 +1,197 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy import exc as sql_exc +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.Camera, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new Camera. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.Camera, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + camera_in: schemas.CameraCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Camera: + try: + camera_in_db = crud.camera.create(db, obj_in=camera_in) + except sql_exc.IntegrityError as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return camera_in_db.to_dict() + + +@router.delete( + "/{camera_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a Camera. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + camera_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.camera.get(db, id=camera_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Camera [id: {camera_id}] not found" + ) + crud.camera.remove(db, id=camera_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{camera_id}", + response_model=schemas.Camera, + status_code=status.HTTP_200_OK, + description=""" +Get a Camera. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Camera, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + camera_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Camera: + camera_in_db = crud.camera.get(db, id=camera_id) + if not camera_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Camera [id: {camera_id}] not found" + ) + return camera_in_db.to_dict() + + +@router.get( + "", + response_model=schemas.Cameras, + status_code=status.HTTP_200_OK, + description=""" +Get all Cameras. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Cameras, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + sn: Optional[str] = Query( + None, alias="sn", description="Filter by camera sn. Fuzzy prefix query is supported" + ), + name: Optional[str] = Query( + None, alias="name", description="Filter by camera name. Fuzzy prefix query is supported" + ), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Cameras: + skip = page_size * (page_num - 1) + total, data = crud.camera.get_multi_with_total( + db, skip=skip, limit=page_size, sn=sn, name=name + ) + return schemas.Cameras(total=total, data=[camera.to_dict() for camera in data]) + + +@router.put( + "/{camera_id}", + response_model=schemas.Camera, + status_code=status.HTTP_200_OK, + description=""" +Update a Camera. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Camera, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + camera_id: int, + camera_in: schemas.CameraUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUModel: + camera_in_db = crud.camera.get(db, id=camera_id) + if not camera_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Camera [id: {camera_id}] not found" + ) + try: + new_camera_in_db = crud.camera.update(db, db_obj=camera_in_db, obj_in=camera_in) + except (sql_exc.DataError, sql_exc.IntegrityError) as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return new_camera_in_db.to_dict() diff --git a/dandelion/api/api_v1/endpoints/cities.py b/dandelion/api/api_v1/endpoints/cities.py new file mode 100644 index 0000000..e03d7bc --- /dev/null +++ b/dandelion/api/api_v1/endpoints/cities.py @@ -0,0 +1,55 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import List + +from fastapi import APIRouter, Depends, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "", + response_model=List[schemas.City], + status_code=status.HTTP_200_OK, + description=""" +Search city by province. +""", + responses={ + status.HTTP_200_OK: {"model": List[schemas.City], "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + province_code: str = Query(..., description="Filter by provinceCode", alias="provinceCode"), + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> List[schemas.City]: + cities = crud.city.get_multi_by_province_code(db, province_code) + return [city for city in cities] diff --git a/dandelion/api/api_v1/endpoints/cloud_homes.py b/dandelion/api/api_v1/endpoints/cloud_homes.py new file mode 100644 index 0000000..7ad1e03 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/cloud_homes.py @@ -0,0 +1,160 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter + +from fastapi import APIRouter, Body, Depends, Query, status +from oslo_log import log +from redis import Redis +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps +from dandelion.util import Optional as Optional_util + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "/online_rate", + response_model=schemas.OnlineRate, + status_code=status.HTTP_200_OK, + description=""" +Get online rate of all devices. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.OnlineRate, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def online_rate( + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.OnlineRate: + rsu_online_rate = { + "online": crud.rsu.get_multi_with_total(db, online_status=True)[0], + "offline": crud.rsu.get_multi_with_total(db, online_status=False)[0], + "notRegister": crud.rsu_tmp.get_multi_with_total(db)[0], + } + # temporarily unavailable data + camera_online_rate = { + "online": 0, + "offline": 0, + "notRegister": 0, + } + # temporarily unavailable data + radar_online_rate = { + "online": 0, + "offline": 0, + "notRegister": 0, + } + return schemas.OnlineRate( + **{ + "data": { + "rsu": rsu_online_rate, + "camera": camera_online_rate, + "radar": radar_online_rate, + } + } + ) + + +@router.get( + "/route_info", + response_model=schemas.RouteInfo, + status_code=status.HTTP_200_OK, + description=""" +Get traffic situation. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RouteInfo, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def route_info( + rsu_esn: str = Query(..., alias="rsuEsn", description="RSU ESN"), + *, + redis_conn: Redis = Depends(deps.get_redis_conn), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RouteInfo: + key = f"ROUTE_INFO_{rsu_esn}" + return schemas.RouteInfo( + vehicleTotal=Optional_util.none(redis_conn.hget(key, "vehicleTotal")) + .map(lambda v: int(v)) + .orElse(0), + averageSpeed=Optional_util.none(redis_conn.hget(key, "averageSpeed")) + .map(lambda v: float(v)) + .map(lambda v: round(v, 1)) + .orElse(0), + pedestrianTotal=Optional_util.none(redis_conn.hget(key, "pedestrianTotal")) + .map(lambda v: int(v)) + .orElse(0), + congestion=Optional_util.none(redis_conn.hget(key, "congestion")) + .map(lambda v: str(v, encoding="utf-8")) + .orElse("Unknown"), + ) + + +@router.post( + "/route_info_push", + response_model=schemas.RouteInfo, + status_code=status.HTTP_201_CREATED, + description=""" +Push traffic situation. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.RouteInfo, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def route_info_push( + route_info_in: schemas.RouteInfoCreate = Body(..., description="Route Info"), + *, + redis_conn: Redis = Depends(deps.get_redis_conn), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RouteInfo: + rsu_esn = route_info_in.rsu_esn + key = f"ROUTE_INFO_{rsu_esn}" + if route_info_in.vehicle_total: + redis_conn.hset(key, "vehicleTotal", route_info_in.vehicle_total) + if route_info_in.average_speed: + redis_conn.hset(key, "averageSpeed", route_info_in.average_speed) + if route_info_in.pedestrian_total: + redis_conn.hset(key, "pedestrianTotal", route_info_in.pedestrian_total) + if route_info_in.congestion: + redis_conn.hset(key, "congestion", route_info_in.congestion) + return schemas.RouteInfo( + vehicleTotal=route_info_in.vehicle_total, + averageSpeed=route_info_in.average_speed, + pedestrianTotal=route_info_in.pedestrian_total, + congestion=route_info_in.congestion, + ) diff --git a/dandelion/api/api_v1/endpoints/countries.py b/dandelion/api/api_v1/endpoints/countries.py new file mode 100644 index 0000000..5bcd7e1 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/countries.py @@ -0,0 +1,58 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import List, Optional + +from fastapi import APIRouter, Depends, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "", + response_model=List[schemas.Country], + status_code=status.HTTP_200_OK, + description=""" +Get all countries list. +""", + responses={ + status.HTTP_200_OK: {"model": List[schemas.Country], "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + cascade: Optional[bool] = Query( + None, alias="cascade", description="Cascade to list all countries." + ), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> List[schemas.Country]: + countries = crud.country.get_multi(db) + if cascade: + return [country.to_all_dict() for country in countries] + return [country.to_dict() for country in countries] diff --git a/dandelion/api/api_v1/endpoints/login.py b/dandelion/api/api_v1/endpoints/login.py new file mode 100644 index 0000000..2d18b5a --- /dev/null +++ b/dandelion/api/api_v1/endpoints/login.py @@ -0,0 +1,122 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import timedelta +from logging import LoggerAdapter + +from fastapi import APIRouter, Body, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from oslo_config import cfg +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import conf, crud, schemas +from dandelion.api import deps +from dandelion.core import security + +LOG: LoggerAdapter = log.getLogger(__name__) +CONF: cfg = conf.CONF + +router = APIRouter() + + +class PasswordRequestForm(OAuth2PasswordRequestForm): + def __init__(self, username: str = Body(...), password: str = Body(...)): + super().__init__( + grant_type="password", + username=username, + password=password, + scope="", + client_id=None, + client_secret=None, + ) + + +@router.post( + "", + response_model=schemas.Token, + status_code=status.HTTP_200_OK, + summary="Login", + description=""" +User login with username and password. +""", + responses={ + 200: {"model": schemas.Token, "description": "OK"}, + 400: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + }, +) +def login( + db: Session = Depends(deps.get_db), + form_data: PasswordRequestForm = Depends(), +) -> schemas.Token: + """Login with the given password and username. + + :param db: db session, defaults to Depends(deps.get_db) + :type db: Session, optional + :param form_data: username and password, defaults to Depends() + :type form_data: PasswordRequestForm, optional + :raises HTTPException: 400 + :return: token + :rtype: Token + """ + LOG.debug(f"Login with username {form_data.username}.") + user = crud.user.authenticate(db, username=form_data.username, password=form_data.password) + if not user: + err = "Incorrect username or password." + LOG.error(err) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=err) + + access_token_expires = timedelta(seconds=CONF.token.expire_seconds) + access_token = security.create_access_token(user.id, expires_delta=access_token_expires) + return schemas.Token(access_token=access_token, token_type="bearer") + + +@router.post( + "/access-token", + response_model=schemas.Token, + summary="Login Access Token(DO NOT USE IN PRODUCTION)", + description=""" +- `DO NOT USE IN PRODUCTION !!!` +- `JUST FOR TESTING PURPOSE !!!` +""", + responses={ + 200: {"model": schemas.Token, "description": "OK"}, + 400: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + }, +) +def access_token( + db: Session = Depends(deps.get_db), + form_data: OAuth2PasswordRequestForm = Depends(), +) -> schemas.Token: + """Creates a new access token. + + :param db: db session, defaults to Depends(deps.get_db) + :type db: Session, optional + :param form_data: form data, defaults to Depends() + :type form_data: OAuth2PasswordRequestForm, optional + :raises HTTPException: 400 + :return: token + :rtype: Token + """ + user = crud.user.authenticate(db, username=form_data.username, password=form_data.password) + if not user: + err = "Incorrect username or password." + LOG.error(err) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=err) + + access_token_expires = timedelta(seconds=CONF.token.expire_seconds) + access_token = security.create_access_token(user.id, expires_delta=access_token_expires) + return schemas.Token(access_token=access_token, token_type="bearer") diff --git a/dandelion/api/api_v1/endpoints/map_rsus.py b/dandelion/api/api_v1/endpoints/map_rsus.py new file mode 100644 index 0000000..cec5508 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/map_rsus.py @@ -0,0 +1,153 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from logging import LoggerAdapter +from typing import Dict, List, Optional, Union + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps +from dandelion.mqtt.service.map.map_down import map_down + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "/{map_id}/rsus", + response_model=schemas.MapRSU, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new Radar. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.MapRSU, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + map_id: int, + map_rsu_in: schemas.MapRSUCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.MapRSU: + map_in_db = crud.map.get(db, id=map_id) + if not map_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Map [id: {map_id}] not found" + ) + + rsus: List[models.RSU] = [] + for rsu_id in map_rsu_in.rsus: + rsu_in_db = crud.rsu.get(db, id=rsu_id) + if not rsu_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"RSU [id: {rsu_id}] not found" + ) + rsus.append(rsu_in_db) + + map_rsus: List[Dict[str, Union[int, str, datetime, bool]]] = [] + for rsu in rsus: + _map_rsu = models.MapRSU(map_id=map_id, rsu_id=rsu.id) + map_rsu = crud.map_rsu.create(db, obj_in=_map_rsu) + map_rsus.append( + { + "id": map_rsu.id, + "rsuId": map_rsu.rsu_id, + "status": map_rsu.status, + "createTime": map_rsu.create_time, + } + ) + map_down(map_in_db.name, map_in_db.data, "v1", rsu.rsu_esn) + + return schemas.MapRSU(**{"data": {"mapId": map_id, "rsus": map_rsus}}) + + +@router.delete( + "/{map_id}/rsus/{map_rsu_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a Map RSU. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + map_id: int, + map_rsu_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.map.get(db, id=map_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Map [id: {map_id}] not found" + ) + if not crud.map_rsu.get(db, id=map_rsu_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"MapRSU [id: {map_rsu_id}] not found" + ) + crud.map_rsu.remove(db, id=map_rsu_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{map_id}/rsus", + response_model=schemas.MapRSUs, + status_code=status.HTTP_200_OK, + description=""" +Get all Map RSUs. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.MapRSUs, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + map_id: Optional[int] = Query(None, alias="mapId", description="Filter by mapId"), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.MapRSUs: + skip = page_size * (page_num - 1) + total, data = crud.map_rsu.get_multi_with_total(db, skip=skip, limit=page_size, map_id=map_id) + return schemas.MapRSUs(total=total, data=[map_rsu.to_dict() for map_rsu in data]) diff --git a/dandelion/api/api_v1/endpoints/maps.py b/dandelion/api/api_v1/endpoints/maps.py new file mode 100644 index 0000000..bf696d9 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/maps.py @@ -0,0 +1,249 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy import exc as sql_exc +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps +from dandelion.util import Optional as Optional_util + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.Map, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new Map. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.Map, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + map_in: schemas.MapCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Map: + new_map_in = models.Map( + name=map_in.name, + area_code=map_in.area_code, + address=map_in.address, + desc=map_in.desc, + data=map_in.data, + lng=Optional_util.none(map_in.data.get("refPos")).map(lambda v: v.get("lon")).orElse(0), + lat=Optional_util.none(map_in.data.get("refPos")).map(lambda v: v.get("lat")).orElse(0), + ) + try: + map_in_db = crud.map.create(db, obj_in=new_map_in) + except sql_exc.IntegrityError as ex: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return map_in_db.to_dict() + + +@router.delete( + "/{map_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a Map. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + map_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.map.get(db, id=map_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Map [id: {map_id}] not found" + ) + crud.map.remove(db, id=map_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{map_id}", + response_model=schemas.Map, + status_code=status.HTTP_200_OK, + description=""" +Get a Radar. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Map, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + map_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Map: + map_in_db = crud.map.get(db, id=map_id) + if not map_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Map [id: {map_id}] not found" + ) + return map_in_db.to_dict() + + +@router.get( + "", + response_model=schemas.Maps, + status_code=status.HTTP_200_OK, + description=""" +Get all Maps. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Maps, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + name: Optional[str] = Query( + None, alias="name", description="Filter by map name. Fuzzy prefix query is supported" + ), + area_code: Optional[str] = Query(None, alias="areaCode", description="Filter by map areaCode"), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Maps: + skip = page_size * (page_num - 1) + total, data = crud.map.get_multi_with_total( + db, skip=skip, limit=page_size, name=name, area_code=area_code + ) + return schemas.Maps(total=total, data=[map.to_dict() for map in data]) + + +@router.put( + "/{map_id}", + response_model=schemas.Map, + status_code=status.HTTP_200_OK, + description=""" +Update a Map. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Map, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + map_id: int, + map_in: schemas.MapUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Map: + map_in_db = crud.map.get(db, id=map_id) + if not map_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Map [id: {map_id}] not found" + ) + + new_map_in = models.Map() + new_map_in.name = map_in.name + new_map_in.area_code = map_in.area_code + new_map_in.address = map_in.address + new_map_in.desc = map_in.desc + if map_in.data: + new_map_in.lng = ( + Optional_util.none(map_in.data.get("refPos")).map(lambda v: v.get("lon")).orElse(0), + ) + new_map_in.lat = ( + Optional_util.none(map_in.data.get("refPos")).map(lambda v: v.get("lat")).orElse(0), + ) + + try: + new_map_in_db = crud.map.update(db, db_obj=map_in_db, obj_in=new_map_in.__dict__) + except (sql_exc.DataError, sql_exc.IntegrityError) as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return new_map_in_db.to_dict() + + +@router.get( + "/{map_id}/data", + response_model=Dict[str, Any], + status_code=status.HTTP_200_OK, + description=""" +Get a Map data. +""", + responses={ + status.HTTP_200_OK: {"model": Dict[str, Any], "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def data( + map_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> Dict[str, Any]: + map_in_db = crud.map.get(db, id=map_id) + if not map_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Map [id: {map_id}] not found" + ) + return map_in_db.data diff --git a/dandelion/api/api_v1/endpoints/mngs.py b/dandelion/api/api_v1/endpoints/mngs.py new file mode 100644 index 0000000..2047b11 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/mngs.py @@ -0,0 +1,200 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from logging import LoggerAdapter +from typing import List, Optional + +from fastapi import APIRouter, Body, Depends, HTTPException, Query, status +from oslo_log import log +from sqlalchemy import exc as sql_exc +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps +from dandelion.mqtt.service.mng import mng_down + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "", + response_model=schemas.MNGs, + status_code=status.HTTP_200_OK, + description=""" +Get all MNGs. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.MNGs, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + rsu_name: Optional[str] = Query( + None, alias="rsuName", description="Filter by rsuName. Fuzzy prefix query is supported" + ), + rsu_esn: Optional[str] = Query(None, alias="rsuEsn", description="Filter by rsuEsn"), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.MNGs: + skip = page_size * (page_num - 1) + total, data = crud.rsu.get_multi_with_total( + db, skip=skip, limit=page_size, rsu_name=rsu_name, rsu_esn=rsu_esn + ) + return schemas.MNGs(total=total, data=[rsu.mng.all_dict() for rsu in data]) + + +@router.put( + "/{mng_id}", + response_model=schemas.MNG, + status_code=status.HTTP_200_OK, + description=""" +Update a MNG. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.MNG, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + mng_id: int, + mng_in: schemas.MNGUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.MNG: + mng_in_db = crud.mng.get(db, id=mng_id) + if not mng_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"MNG [id: {mng_id}] not found" + ) + try: + new_mng_in_db = crud.mng.update_mng(db, db_obj=mng_in_db, obj_in=mng_in) + except (sql_exc.DataError, sql_exc.IntegrityError) as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return new_mng_in_db.all_dict() + + +@router.post( + "/{mng_id}/down", + response_model=schemas.Message, + status_code=status.HTTP_200_OK, + description=""" +Down a MNG. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Message, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def down( + mng_id: int = Query(..., alias="mng_id", gt=0, description="MNG id"), + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Message: + mng_in_db = crud.mng.get(db, id=mng_id) + if not mng_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"MNG [id: {mng_id}] not found" + ) + + data = mng_in_db.mqtt_dict() + data["timestamp"] = int(time.time()) + data["ack"] = False + mng_down(mng_in_db.rsu.rsu_esn, data) + return schemas.Message(detail="Send down for mng.") + + +@router.post( + "/{mng_id}/copy", + response_model=schemas.Message, + status_code=status.HTTP_200_OK, + description=""" +Copy a MNG. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Message, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def copy( + mng_id: int = Query(..., alias="mng_id", gt=0, description="MNG id"), + mng_copy_in: schemas.MNGCopy = Body(..., alias="mng_copy_in", description="MNG copy"), + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Message: + mng_in_db = crud.mng.get(db, id=mng_id) + if not mng_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"MNG [id: {mng_id}] not found" + ) + + new_mngs: List[models.MNG] = [] + for rsu_id in mng_copy_in.rsus: + new_mng_in_db = crud.mng.get_by_rsu_id(db, rsu_id=rsu_id) + if not new_mng_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"MNG [by rsu id: {rsu_id}] not found", + ) + new_mngs.append(new_mng_in_db) + + timestamp = int(time.time()) + for new_mng in new_mngs: + new_mng = crud.mng.update( + db, + db_obj=new_mng, + obj_in={ + "heartbeat_rate": mng_in_db.heartbeat_rate, + "running_info_rate": mng_in_db.running_info_rate, + "log_level": mng_in_db.log_level, + "reboot": mng_in_db.reboot, + "address_change": mng_in_db.address_change, + "extend_config": mng_in_db.extend_config, + }, + ) + data = new_mng.mqtt_dict() + data["timestamp"] = timestamp + data["ack"] = False + mng_down(new_mng.rsu.rsu_esn, data) + + return schemas.Message(detail="Copy mng.") diff --git a/dandelion/api/api_v1/endpoints/provinces.py b/dandelion/api/api_v1/endpoints/provinces.py new file mode 100644 index 0000000..2aa11dd --- /dev/null +++ b/dandelion/api/api_v1/endpoints/provinces.py @@ -0,0 +1,55 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import List + +from fastapi import APIRouter, Depends, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "", + response_model=List[schemas.Province], + status_code=status.HTTP_200_OK, + description=""" +Search province by country. +""", + responses={ + status.HTTP_200_OK: {"model": List[schemas.Province], "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + country_code: str = Query(..., description="Filter by countryCode", alias="countryCode"), + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> List[schemas.Province]: + provinces = crud.province.get_multi_by_country_code(db, country_code) + return [province for province in provinces] diff --git a/dandelion/api/api_v1/endpoints/radars.py b/dandelion/api/api_v1/endpoints/radars.py new file mode 100644 index 0000000..132e639 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/radars.py @@ -0,0 +1,194 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy import exc as sql_exc +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.Radar, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new Radar. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.Radar, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + radar_in: schemas.RadarCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Radar: + radar_in_db = crud.radar.create(db, obj_in=radar_in) + return radar_in_db.to_dict() + + +@router.delete( + "/{radar_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a Radar. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + radar_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.radar.get(db, id=radar_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Radar [id: {radar_id}] not found" + ) + crud.radar.remove(db, id=radar_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{radar_id}", + response_model=schemas.Radar, + status_code=status.HTTP_200_OK, + description=""" +Get a Radar. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Radar, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + radar_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Radar: + radar_in_db = crud.radar.get(db, id=radar_id) + if not radar_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Radar [id: {radar_id}] not found" + ) + return radar_in_db.to_dict() + + +@router.get( + "", + response_model=schemas.Radars, + status_code=status.HTTP_200_OK, + description=""" +Get all Radars. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Radars, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + sn: Optional[str] = Query( + None, alias="sn", description="Filter by radar sn. Fuzzy prefix query is supported" + ), + name: Optional[str] = Query( + None, alias="name", description="Filter by radar name. Fuzzy prefix query is supported" + ), + rsu_id: Optional[int] = Query(None, alias="rsuId", description="Filter by rsuId"), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Radars: + skip = page_size * (page_num - 1) + total, data = crud.radar.get_multi_with_total( + db, skip=skip, limit=page_size, sn=sn, name=name, rsu_id=rsu_id + ) + return schemas.Radars(total=total, data=[radar.to_dict() for radar in data]) + + +@router.put( + "/{radar_id}", + response_model=schemas.Radar, + status_code=status.HTTP_200_OK, + description=""" +Update a Radar. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.Radar, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + radar_id: int, + radar_in: schemas.RadarUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.Radar: + radar_in_db = crud.radar.get(db, id=radar_id) + if not radar_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"Radar [id: {radar_id}] not found" + ) + try: + new_radar_in_db = crud.radar.update(db, db_obj=radar_in_db, obj_in=radar_in) + except (sql_exc.DataError, sql_exc.IntegrityError) as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return new_radar_in_db.to_dict() diff --git a/dandelion/api/api_v1/endpoints/rsi_events.py b/dandelion/api/api_v1/endpoints/rsi_events.py new file mode 100644 index 0000000..fccdf86 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsi_events.py @@ -0,0 +1,94 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "/{event_id}", + response_model=schemas.RSIEvent, + status_code=status.HTTP_200_OK, + description=""" +Get a RSIEvent. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSIEvent, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + event_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSIEvent: + rsi_event_in_db = crud.rsi_event.get(db, id=event_id) + if not rsi_event_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"RSIEvent [id: {event_id}] not found" + ) + return rsi_event_in_db.to_all_dict() + + +@router.get( + "", + response_model=schemas.RSIEvents, + status_code=status.HTTP_200_OK, + description=""" +Get all RSI Events. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSIEvents, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + event_type: Optional[int] = Query(None, alias="eventType", description="Filter by eventType"), + area_code: Optional[str] = Query(None, alias="areaCode", description="Filter by areaCode"), + address: Optional[str] = Query( + None, alias="address", description="Filter by address. Fuzzy prefix query is supported" + ), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSIEvents: + skip = page_size * (page_num - 1) + total, data = crud.rsi_event.get_multi_with_total( + db, skip=skip, limit=page_size, event_type=event_type, area_code=area_code, address=address + ) + return schemas.RSIEvents(total=total, data=[rsi_event.to_all_dict() for rsi_event in data]) diff --git a/dandelion/api/api_v1/endpoints/rsm_participants.py b/dandelion/api/api_v1/endpoints/rsm_participants.py new file mode 100644 index 0000000..92a09f6 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsm_participants.py @@ -0,0 +1,61 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Optional + +from fastapi import APIRouter, Depends, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "", + response_model=schemas.RSMParticipants, + status_code=status.HTTP_200_OK, + description=""" +Get all RSM. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSMParticipants, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + ptc_type: Optional[int] = Query(None, alias="ptcType", description="Filter by ptcType"), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSMParticipants: + skip = page_size * (page_num - 1) + total, data = crud.rsm_participant.get_multi_with_total( + db, skip=skip, limit=page_size, ptc_type=ptc_type + ) + return schemas.RSMParticipants( + total=total, data=[rsm_participant.to_dict() for rsm_participant in data] + ) diff --git a/dandelion/api/api_v1/endpoints/rsu_configs.py b/dandelion/api/api_v1/endpoints/rsu_configs.py new file mode 100644 index 0000000..24df84e --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsu_configs.py @@ -0,0 +1,230 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import List, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy import exc as sql_exc +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps +from dandelion.mqtt.service.rsu.rsu_config import config_down + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.RSUConfig, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new RSU Config. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.RSUConfig, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + rsu_config_in: schemas.RSUConfigCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUConfig: + rsus: List[models.RSU] = [] + if rsu_config_in.rsus: + for rsu_id in rsu_config_in.rsus: + rus_in_db = crud.rsu.get(db, id=rsu_id) + if not rus_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU [id: {rsu_id}] not found", + ) + rsus.append(rus_in_db) + + try: + config_in_db = crud.rsu_config.create_rsu_config(db, obj_in=rsu_config_in, rsus=rsus) + except sql_exc.IntegrityError as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + + # config down + data = config_in_db.mqtt_dict() + data["ack"] = False + for rsu in rsus: + config_down(data, rsu.rsu_esn) + + return config_in_db.to_dict() + + +@router.delete( + "/{rsu_config_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a RSUConfig. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + rsu_config_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.rsu_config.get(db, id=rsu_config_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSUConfig [id: {rsu_config_id}] not found", + ) + crud.rsu_config.remove(db, id=rsu_config_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{rsu_config_id}", + response_model=schemas.RSUConfigWithRSUs, + status_code=status.HTTP_200_OK, + description=""" +Get a RSUConfig. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUConfigWithRSUs, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + rsu_config_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUConfigWithRSUs: + rsu_config_in_db = crud.rsu_config.get(db, id=rsu_config_id) + if not rsu_config_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSUConfig [id: {rsu_config_id}] not found", + ) + return rsu_config_in_db.to_all_dict() + + +@router.get( + "", + response_model=schemas.RSUConfigs, + status_code=status.HTTP_200_OK, + description=""" +Get all RSUConfigs. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUConfigs, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + name: Optional[str] = Query( + None, alias="name", description="Filter by name. Fuzzy prefix query is supported" + ), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUConfigs: + skip = page_size * (page_num - 1) + total, data = crud.rsu_config.get_multi_with_total(db, skip=skip, limit=page_size, name=name) + return schemas.RSUConfigs(total=total, data=[rsu_config.to_dict() for rsu_config in data]) + + +@router.put( + "/{rsu_config_id}", + response_model=schemas.RSUConfig, + status_code=status.HTTP_200_OK, + description=""" +Update a Radar. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUConfig, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + rsu_config_id: int, + rsu_config_in: schemas.RSUConfigUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUConfig: + rsu_config_in_db = crud.rsu_config.get(db, id=rsu_config_id) + if not rsu_config_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSUConfig [id: {rsu_config_id}] not found", + ) + + rsus: List[models.RSU] = [] + if rsu_config_in.rsus: + for rsu_id in rsu_config_in.rsus: + rus_in_db = crud.rsu.get(db, id=rsu_id) + if not rus_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU [id: {rsu_id}] not found", + ) + rsus.append(rus_in_db) + + try: + crud.rsu_config_rsu.remove_by_rsu_config_id(db, rsu_config_id=rsu_config_id) + LOG.debug(f"Old RSU Config in db: {rsu_config_in_db.to_dict()}") + new_rsu_config_in_db = crud.rsu_config.update_rsu_config( + db, db_obj=rsu_config_in_db, obj_in=rsu_config_in, rsus=rsus + ) + except (sql_exc.DataError, sql_exc.IntegrityError) as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return new_rsu_config_in_db.to_dict() diff --git a/dandelion/api/api_v1/endpoints/rsu_logs.py b/dandelion/api/api_v1/endpoints/rsu_logs.py new file mode 100644 index 0000000..1fdd550 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsu_logs.py @@ -0,0 +1,223 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +from logging import LoggerAdapter +from typing import List + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps +from dandelion.mqtt.service.log import log_down + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.RSULog, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new RSU log. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.RSULog, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + rsu_log_in: schemas.RSULogCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSULog: + rsus: List[models.RSU] = [] + for rsu_id in rsu_log_in.rsus: + rus_in_db = crud.rsu.get(db, id=rsu_id) + if not rus_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU [id: {rsu_id}] not found", + ) + rsus.append(rus_in_db) + + log_in_db = crud.rsu_log.create_rsu_log(db, obj_in=rsu_log_in, rsus=rsus) + + timestamp = int(time.time()) + for rsu in rsus: + data = log_in_db.mqtt_dict() + data["rsuId"] = rsu.rsu_id + data["rsuEsn"] = rsu.rsu_esn + data["protocolVersion"] = rsu.version + data["timestamp"] = timestamp + data["ack"] = False + log_down(data, rsu.rsu_esn) + return log_in_db.to_all_dict() + + +@router.delete( + "/{rsu_log_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a RSULog. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + rsu_log_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.rsu_log.get(db, id=rsu_log_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"RSULog [id: {rsu_log_id}] not found" + ) + crud.rsu_log.remove(db, id=rsu_log_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{rsu_log_id}", + response_model=schemas.RSULog, + status_code=status.HTTP_200_OK, + description=""" +Get a RSULog. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSULog, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + rsu_log_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSULog: + rsu_log_in_db = crud.rsu_log.get(db, id=rsu_log_id) + if not rsu_log_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"RSULog [id: {rsu_log_id}] not found" + ) + return rsu_log_in_db.to_all_dict() + + +@router.get( + "", + response_model=schemas.RSULogs, + status_code=status.HTTP_200_OK, + description=""" +Get all RSULogs. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSULogs, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSULogs: + skip = page_size * (page_num - 1) + total, data = crud.rsu_log.get_multi_with_total(db, skip=skip, limit=page_size) + return schemas.RSULogs(total=total, data=[rsu_log.to_all_dict() for rsu_log in data]) + + +@router.put( + "/{rsu_log_id}", + response_model=schemas.RSULog, + status_code=status.HTTP_200_OK, + description=""" +Update a RSULog. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSULog, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + rsu_log_id: int, + rsu_log_in: schemas.RSULogUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSULog: + rsu_log_in_db = crud.rsu_log.get(db, id=rsu_log_id) + if not rsu_log_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"RSULog [id: {rsu_log_id}] not found" + ) + + rsus: List[models.RSU] = [] + for rsu_id in rsu_log_in.rsus: + rus_in_db = crud.rsu.get(db, id=rsu_id) + if not rus_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU [id: {rsu_id}] not found", + ) + rsus.append(rus_in_db) + + log_in_db = crud.rsu_log.update_rsu_log(db, db_obj=rsu_log_in_db, obj_in=rsu_log_in, rsus=rsus) + + timestamp = int(time.time()) + for rsu in rsus: + data = log_in_db.mqtt_dict() + data["rsuId"] = rsu.rsu_id + data["rsuEsn"] = rsu.rsu_esn + data["protocolVersion"] = rsu.version + data["timestamp"] = timestamp + data["ack"] = False + log_down(data, rsu.rsu_esn) + return log_in_db.to_all_dict() diff --git a/dandelion/api/api_v1/endpoints/rsu_models.py b/dandelion/api/api_v1/endpoints/rsu_models.py new file mode 100644 index 0000000..33c5ba0 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsu_models.py @@ -0,0 +1,193 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.RSUModel, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new RSU Model. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.RSUModel, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + rsu_model_in: schemas.RSUModelCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUModel: + rsu_model_in_db = crud.rsu_model.create(db, obj_in=rsu_model_in) + return rsu_model_in_db.to_dict() + + +@router.delete( + "/{rsu_model_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a RSU Model. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + rsu_model_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.rsu_model.get(db, id=rsu_model_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU Model [id: {rsu_model_id}] not found", + ) + crud.rsu_model.remove(db, id=rsu_model_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{rsu_model_id}", + response_model=schemas.RSUModel, + status_code=status.HTTP_200_OK, + description=""" +Get a RSU Model. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUModel, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + rsu_model_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUModel: + rsu_model_in_db = crud.rsu_model.get(db, id=rsu_model_id) + if not rsu_model_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU Model [id: {rsu_model_id}] not found", + ) + return rsu_model_in_db.to_dict() + + +@router.get( + "", + response_model=schemas.RSUModels, + status_code=status.HTTP_200_OK, + description=""" +Get all RSU Models. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUModels, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + name: Optional[str] = Query( + None, alias="name", description="Filter by name. Fuzzy prefix query is supported" + ), + manufacturer: Optional[str] = Query( + None, + alias="manufacturer", + description="Filter by manufacturer. Fuzzy prefix query is supported", + ), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUModels: + skip = page_size * (page_num - 1) + total, data = crud.rsu_model.get_multi_with_total( + db, skip=skip, limit=page_size, name=name, manufacturer=manufacturer + ) + return schemas.RSUModels(total=total, data=[rsu_model.to_dict() for rsu_model in data]) + + +@router.put( + "/{rsu_model_id}", + response_model=schemas.RSUModel, + status_code=status.HTTP_200_OK, + description=""" +Update a RSU Model. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUModel, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + rsu_model_id: int, + rsu_model_in: schemas.RSUModelUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUModel: + rsu_model_in_db = crud.rsu_model.get(db, id=rsu_model_id) + if not rsu_model_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU Model [id: {rsu_model_id}] not found", + ) + new_rsu_model_in_db = crud.rsu_model.update(db, db_obj=rsu_model_in_db, obj_in=rsu_model_in) + return new_rsu_model_in_db.to_dict() diff --git a/dandelion/api/api_v1/endpoints/rsu_queries.py b/dandelion/api/api_v1/endpoints/rsu_queries.py new file mode 100644 index 0000000..1336e55 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsu_queries.py @@ -0,0 +1,127 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.RSUQuery, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new RSU Query. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.RSUQuery, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + rsu_query_in: schemas.RSUQueryCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUQuery: + for rsu_id in rsu_query_in.rsus: + rus_in_db = crud.rsu.get(db, id=rsu_id) + if not rus_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU [id: {rsu_id}] not found", + ) + + rsu_query_in_db = crud.rsu_query.create(db, obj_in=rsu_query_in) + for rsu_id in rsu_query_in.rsus: + crud.rsu_query_result.create( + db, obj_in=schemas.RSUQueryResultCreate(query_id=rsu_query_in_db.id, rsu_id=rsu_id) + ) + return rsu_query_in_db.to_dict() + + +@router.get( + "/{rsu_query_id}", + response_model=schemas.RSUQueryDetail, + status_code=status.HTTP_200_OK, + description=""" +Get a RSU Query. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUQueryDetail, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + rsu_query_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUQueryDetail: + rsu_query_in_db = crud.rsu_query.get(db, id=rsu_query_id) + if not rsu_query_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU Query [id: {rsu_query_id}] not found", + ) + return schemas.RSUQueryDetail(**{"data": [v.to_all_dict() for v in rsu_query_in_db.results]}) + + +@router.get( + "", + response_model=schemas.RSUQueries, + status_code=status.HTTP_200_OK, + description=""" +Get all RSUQueries. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUQueries, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUQueries: + skip = page_size * (page_num - 1) + total, data = crud.rsu_query.get_multi_with_total(db, skip=skip, limit=page_size) + return schemas.RSUQueries(total=total, data=[rsu_query.to_dict() for rsu_query in data]) diff --git a/dandelion/api/api_v1/endpoints/rsu_tmps.py b/dandelion/api/api_v1/endpoints/rsu_tmps.py new file mode 100644 index 0000000..2b7fc24 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsu_tmps.py @@ -0,0 +1,62 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Optional + +from fastapi import APIRouter, Depends, Query, status +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.get( + "", + response_model=schemas.RSUTMPs, + status_code=status.HTTP_200_OK, + description=""" +Get all TMP RSUs. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUTMPs, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + rsu_name: Optional[str] = Query( + None, alias="rsuName", description="Filter by rsuName. Fuzzy prefix query is supported" + ), + rsu_esn: Optional[str] = Query(None, alias="rsuEsn", description="Filter by rsuEsn"), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUTMPs: + skip = page_size * (page_num - 1) + total, data = crud.rsu_tmp.get_multi_with_total( + db, skip=skip, limit=page_size, rsu_name=rsu_name, rsu_esn=rsu_esn + ) + return schemas.RSUTMPs(total=total, data=[rsu_tmp.to_dict() for rsu_tmp in data]) diff --git a/dandelion/api/api_v1/endpoints/rsus.py b/dandelion/api/api_v1/endpoints/rsus.py new file mode 100644 index 0000000..756c148 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/rsus.py @@ -0,0 +1,287 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import JSONResponse +from oslo_log import log +from sqlalchemy import exc as sql_exc +from sqlalchemy.orm import Session, exc as orm_exc + +from dandelion import crud, models, schemas +from dandelion.api import deps +from dandelion.util import Optional as Optional_util + +router = APIRouter() +LOG: LoggerAdapter = log.getLogger(__name__) + + +@router.post( + "", + response_model=schemas.RSU, + status_code=status.HTTP_201_CREATED, + description=""" +Create a new RSU. +""", + responses={ + status.HTTP_201_CREATED: {"model": schemas.RSU, "description": "Created"}, + status.HTTP_400_BAD_REQUEST: {"model": schemas.ErrorMessage, "description": "Bad Request"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def create( + rsu_in: schemas.RSUCreate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSU: + if rsu_in.tmp_id: + try: + crud.rsu_tmp.remove(db, id=rsu_in.tmp_id) + except orm_exc.UnmappedInstanceError: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU Temp [id: {rsu_in.tmp_id}] not found", + ) + del rsu_in.tmp_id + try: + rsu_in_db = crud.rsu.create(db, obj_in=rsu_in) + except sql_exc.IntegrityError as ex: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return rsu_in_db.to_all_dict() + + +@router.get( + "", + response_model=schemas.RSUs, + status_code=status.HTTP_200_OK, + description=""" +Get all RSUs. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUs, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def list( + rsu_name: Optional[str] = Query( + None, alias="rsuName", description="Filter by rsuName. Fuzzy prefix query is supported" + ), + rsu_esn: Optional[str] = Query( + None, alias="rsuEsn", description="Filter by rsuEsn. Fuzzy prefix query is supported" + ), + area_code: Optional[str] = Query(None, alias="areaCode", description="Filter by areaCode"), + online_status: Optional[bool] = Query( + None, alias="onlineStatus", description="Filter by onlineStatus" + ), + rsu_status: Optional[bool] = Query(None, alias="rsuStatus", description="Filter by rsuStatus"), + page_num: int = Query(1, alias="pageNum", gt=0, description="Page number"), + page_size: int = Query(10, alias="pageSize", gt=0, description="Page size"), + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUs: + skip = page_size * (page_num - 1) + total, data = crud.rsu.get_multi_with_total( + db, + skip=skip, + limit=page_size, + rsu_name=rsu_name, + rsu_esn=rsu_esn, + area_code=area_code, + online_status=online_status, + rsu_status=rsu_status, + ) + return schemas.RSUs(total=total, data=[rsu.to_all_dict() for rsu in data]) + + +@router.get( + "/{rsu_id}", + response_model=schemas.RSUDetail, + status_code=status.HTTP_200_OK, + description=""" +Get a RSU. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSUDetail, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + rsu_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSUDetail: + rsu_in_db = crud.rsu.get(db, id=rsu_id) + if not rsu_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU [id: {rsu_in_db}] not found", + ) + result = rsu_in_db.to_info_dict() + rsu_config_rsus: List[models.RSUConfigRSU] = result["config"] + result["config"] = [rsu_config_rsu.to_dict() for rsu_config_rsu in rsu_config_rsus] + + return result + + +@router.patch( + "/{rsu_id}", + response_model=schemas.RSU, + status_code=status.HTTP_200_OK, + description=""" +Update a RSU. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSU, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def update( + rsu_id: int, + rsu_in: schemas.RSUUpdate, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSU: + rsu_in_db = crud.rsu.get(db, id=rsu_id) + if not rsu_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"RSU [id: {rsu_id}] not found" + ) + try: + new_rsu_in_db = crud.rsu.update(db, db_obj=rsu_in_db, obj_in=rsu_in) + except (sql_exc.DataError, sql_exc.IntegrityError) as ex: + LOG.error(ex.args[0]) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ex.args[0]) + return new_rsu_in_db.to_all_dict() + + +@router.delete( + "/{rsu_id}", + status_code=status.HTTP_204_NO_CONTENT, + description=""" +Delete a RSU. +""", + responses={ + status.HTTP_204_NO_CONTENT: {"class": JSONResponse, "description": "No Content"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, + response_class=JSONResponse, +) +def delete( + rsu_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> JSONResponse: + if not crud.rsu.get(db, id=rsu_id): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"RSU [id: {rsu_id}] not found" + ) + crud.rsu.remove(db, id=rsu_id) + return JSONResponse(content=None, status_code=status.HTTP_204_NO_CONTENT) + + +@router.get( + "/{rsu_esn}/location", + response_model=schemas.RSULocation, + status_code=status.HTTP_200_OK, + description=""" +Get a RSU Location. +""", + responses={ + status.HTTP_200_OK: {"model": schemas.RSULocation, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get_location( + rsu_esn: str, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.RSULocation: + rsu_in_db = crud.rsu.get_by_rsu_esn(db, rsu_esn=rsu_esn) + if not rsu_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"RSU [rsu_esn: {rsu_esn}] not found", + ) + return Optional_util.none(rsu_in_db).map(lambda v: v.location).get() + + +@router.get( + "/{rsu_id}/map", + response_model=Dict[str, Any], + status_code=status.HTTP_200_OK, + description=""" +Get a RSU Map. +""", + responses={ + status.HTTP_200_OK: {"model": Dict[str, Any], "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get_map( + rsu_id: int, + *, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> Dict[str, Any]: + map_rsu_in_db = crud.map_rsu.get_by_rsu_id(db, rsu_id=rsu_id) + if not map_rsu_in_db: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Map RSU [rsu_id: {rsu_id}] not found", + ) + return Optional_util.none(map_rsu_in_db).map(lambda v: v.map).map(lambda v: v.data).get() diff --git a/dandelion/api/api_v1/endpoints/users.py b/dandelion/api/api_v1/endpoints/users.py new file mode 100644 index 0000000..8d5f927 --- /dev/null +++ b/dandelion/api/api_v1/endpoints/users.py @@ -0,0 +1,68 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.api import deps + +router = APIRouter() + + +@router.post("", response_model=schemas.User) +def create( + *, + db: Session = Depends(deps.get_db), + user_in: schemas.UserCreate, +) -> Any: + """ + Create new user. + """ + user = crud.user.get_by_username(db, username=user_in.username) + if user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The user with this username already exists in the system.", + ) + user = crud.user.create(db, obj_in=user_in) + return user + + +@router.get( + "/me", + response_model=schemas.User, + status_code=status.HTTP_200_OK, + description=""" +Get detailed info of me(current login user). +""", + responses={ + status.HTTP_200_OK: {"model": schemas.User, "description": "OK"}, + status.HTTP_401_UNAUTHORIZED: { + "model": schemas.ErrorMessage, + "description": "Unauthorized", + }, + status.HTTP_403_FORBIDDEN: {"model": schemas.ErrorMessage, "description": "Forbidden"}, + status.HTTP_404_NOT_FOUND: {"model": schemas.ErrorMessage, "description": "Not Found"}, + }, +) +def get( + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +) -> schemas.User: + return current_user diff --git a/dandelion/api/deps.py b/dandelion/api/deps.py new file mode 100644 index 0000000..5de614a --- /dev/null +++ b/dandelion/api/deps.py @@ -0,0 +1,65 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Generator + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from jose import jwt +from oslo_config import cfg +from oslo_log import log +from pydantic import ValidationError +from redis import Redis +from sqlalchemy.orm import Session + +from dandelion import conf, constants, crud, models, schemas +from dandelion.db import redis_pool, session + +LOG: LoggerAdapter = log.getLogger(__name__) +CONF: cfg = conf.CONF + +reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{constants.API_V1_STR}/login/access-token") + + +def get_db() -> Generator: + try: + db: Session = session.DB_SESSION_LOCAL() + yield db + finally: + db.close() + + +def get_redis_conn() -> Redis: + return redis_pool.REDIS_CONN + + +def get_current_user( + db: Session = Depends(get_db), token: str = Depends(reusable_oauth2) +) -> models.User: + try: + payload = jwt.decode(token, CONF.token.secret_key, algorithms=[constants.ALGORITHM]) + token_data = schemas.TokenPayload(**payload) + except (jwt.JWTError, ValidationError): + err = "Could not validate credentials." + LOG.error(err) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=err) + user = crud.user.get(db, id=token_data.sub) + if not user: + err = "User not found." + LOG.error(err) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=err) + return user diff --git a/dandelion/conf/__init__.py b/dandelion/conf/__init__.py new file mode 100644 index 0000000..406deba --- /dev/null +++ b/dandelion/conf/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from oslo_config import cfg + +from dandelion.conf import cors, database, mqtt, redis, token + +CONF: cfg = cfg.CONF + + +cors.register_opts(CONF) +database.register_opts(CONF) +mqtt.register_opts(CONF) +redis.register_opts(CONF) +token.register_opts(CONF) diff --git a/dandelion/conf/cors.py b/dandelion/conf/cors.py new file mode 100644 index 0000000..a355c70 --- /dev/null +++ b/dandelion/conf/cors.py @@ -0,0 +1,44 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from oslo_config import cfg + +cors_group = cfg.OptGroup( + name="cors", + title="CORS Options", + help=""" +CORS related options. +""", +) + +cors_opts = [ + cfg.ListOpt( + "origins", + default=[], + help=""" +CORS origins. +""", + ), +] + + +def register_opts(conf): + conf.register_group(cors_group) + conf.register_opts(cors_opts, group=cors_group) + + +def list_opts(): + return {cors_group: cors_opts} diff --git a/dandelion/conf/database.py b/dandelion/conf/database.py new file mode 100644 index 0000000..89079c2 --- /dev/null +++ b/dandelion/conf/database.py @@ -0,0 +1,44 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from oslo_config import cfg + +database_group = cfg.OptGroup( + name="database", + title="Database Options", + help=""" +Database related options. +""", +) + +database_opts = [ + cfg.StrOpt( + "connection", + default="sqlite:////tmp/dandelion.db", + help=""" +Connection of database. +""", + ), +] + + +def register_opts(conf): + conf.register_group(database_group) + conf.register_opts(database_opts, group=database_group) + + +def list_opts(): + return {database_group: database_opts} diff --git a/dandelion/conf/mqtt.py b/dandelion/conf/mqtt.py new file mode 100644 index 0000000..53aba1c --- /dev/null +++ b/dandelion/conf/mqtt.py @@ -0,0 +1,62 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from oslo_config import cfg + +mqtt_group = cfg.OptGroup( + name="mqtt", + title="MQTT Options", + help=""" +MQTT related options. +""", +) + +mqtt_opts = [ + cfg.IPOpt( + "host", + help=""" +Host of MQTT server. +""", + ), + cfg.PortOpt( + "port", + default=1883, + help=""" +Port of MQTT server. +""", + ), + cfg.StrOpt( + "username", + help=""" +Username of MQTT server. +""", + ), + cfg.StrOpt( + "password", + help=""" +Password for username of MQTT server. +""", + ), +] + + +def register_opts(conf): + conf.register_group(mqtt_group) + conf.register_opts(mqtt_opts, group=mqtt_group) + + +def list_opts(): + return {mqtt_group: mqtt_opts} diff --git a/dandelion/conf/opts.py b/dandelion/conf/opts.py new file mode 100644 index 0000000..b5952f1 --- /dev/null +++ b/dandelion/conf/opts.py @@ -0,0 +1,72 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import collections +import importlib +import os +import pkgutil +from types import ModuleType +from typing import Any, List, Tuple + +LIST_OPTS_FUNC_NAME = "list_opts" + + +def _tupleize(dct: collections.defaultdict) -> List[Tuple[str, Any]]: + """Take the dict of options and convert to the 2-tuple format.""" + return [(key, val) for key, val in dct.items()] + + +def list_opts() -> List[Tuple[str, Any]]: + opts: collections.defaultdict = collections.defaultdict(list) + module_names = _list_module_names() + imported_modules = _import_modules(module_names) + _append_config_options(imported_modules, opts) + return _tupleize(opts) + + +def _list_module_names() -> List[str]: + module_names = [] + package_path = os.path.dirname(os.path.abspath(__file__)) + for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]): + if modname == "opts" or ispkg: + continue + else: + module_names.append(modname) + return module_names + + +def _import_modules(module_names: List[str]) -> List[ModuleType]: + imported_modules = [] + for modname in module_names: + mod = importlib.import_module("dandelion.conf." + modname) + if not hasattr(mod, LIST_OPTS_FUNC_NAME): + msg = ( + "The module 'dandelion.conf.%s' should have a '%s' " + "function which returns the config options." % (modname, LIST_OPTS_FUNC_NAME) + ) + raise Exception(msg) + else: + imported_modules.append(mod) + return imported_modules + + +def _append_config_options( + imported_modules: List[ModuleType], config_options: collections.defaultdict +) -> None: + for mod in imported_modules: + configs = mod.list_opts() + for key, val in configs.items(): + config_options[key].extend(val) diff --git a/dandelion/conf/redis.py b/dandelion/conf/redis.py new file mode 100644 index 0000000..487bd59 --- /dev/null +++ b/dandelion/conf/redis.py @@ -0,0 +1,47 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from oslo_config import cfg + +redis_group = cfg.OptGroup( + name="redis", + title="Redis Options", + help=""" +Redis related options. +""", +) + +redis_opts = [ + cfg.StrOpt( + "connection", + help=""" +Connection of redis. +If you have a single redis server, you can set connection as followed: +"redis://:@:?db=0&socket_timeout=60&retry_on_timeout=yes". +If you have a sentinel redis cluster, you can set connection as followed: +"redis://:@:?sentinel=&sentinel_fallback=:&sentinel_fallback=:&db=0&socket_timeout=60&retry_on_timeout=yes" +""", + ), +] + + +def register_opts(conf): + conf.register_group(redis_group) + conf.register_opts(redis_opts, group=redis_group) + + +def list_opts(): + return {redis_group: redis_opts} diff --git a/dandelion/conf/token.py b/dandelion/conf/token.py new file mode 100644 index 0000000..2fb230d --- /dev/null +++ b/dandelion/conf/token.py @@ -0,0 +1,51 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from oslo_config import cfg + +token_group = cfg.OptGroup( + name="token", + title="Token Options", + help=""" +Token related options. +""", +) + +token_opts = [ + cfg.IntOpt( + "expire_seconds", + default=604800, + help=""" +Token expire seonds. Default: 604800(7 days). +""", + ), + cfg.StrOpt( + "secret_key", + default="CP7l45i1SEk7jues8DAcO3MnWe-NMKITz3XrMxHBZhY", + help=""" +Secret key of token. +""", + ), +] + + +def register_opts(conf): + conf.register_group(token_group) + conf.register_opts(token_opts, group=token_group) + + +def list_opts(): + return {token_group: token_opts} diff --git a/dandelion/constants.py b/dandelion/constants.py new file mode 100644 index 0000000..5b6bf61 --- /dev/null +++ b/dandelion/constants.py @@ -0,0 +1,20 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +PROJECT_NAME: str = "dandelion" +CONFIG_FILE_PATH: str = "/etc/dandelion/dandelion.conf" +API_V1_STR: str = "/api/v1" +ALGORITHM: str = "HS256" diff --git a/dandelion/core/__init__.py b/dandelion/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/core/security.py b/dandelion/core/security.py new file mode 100644 index 0000000..d4dcfc5 --- /dev/null +++ b/dandelion/core/security.py @@ -0,0 +1,49 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime, timedelta +from typing import Any, Optional, Union + +from jose import jwt +from oslo_config import cfg +from passlib.context import CryptContext + +from dandelion import constants + +CONF = cfg.CONF + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def create_access_token( + subject: Union[str, Any], expires_delta: Optional[timedelta] = None +) -> str: + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(seconds=CONF.token.expire_seconds) + to_encode = {"exp": expire, "sub": str(subject)} + jwt_token = jwt.encode(to_encode, CONF.token.secret_key, algorithm=constants.ALGORITHM) + return jwt_token + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) diff --git a/dandelion/crud/__init__.py b/dandelion/crud/__init__.py new file mode 100644 index 0000000..b9eed75 --- /dev/null +++ b/dandelion/crud/__init__.py @@ -0,0 +1,61 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from .crud_area import area +from .crud_camera import camera +from .crud_city import city +from .crud_country import country +from .crud_map import map +from .crud_map_rsu import map_rsu +from .crud_mng import mng +from .crud_province import province +from .crud_radar import radar +from .crud_rsi_event import rsi_event +from .crud_rsm import rsm +from .crud_rsm_participant import rsm_participant +from .crud_rsu import rsu +from .crud_rsu_config import rsu_config +from .crud_rsu_config_rsu import rsu_config_rsu +from .crud_rsu_log import rsu_log +from .crud_rsu_model import rsu_model +from .crud_rsu_query import rsu_query +from .crud_rsu_query_result import rsu_query_result +from .crud_rsu_tmp import rsu_tmp +from .crud_user import user + +__all__ = [ + "user", + "country", + "area", + "city", + "province", + "rsu", + "rsu_tmp", + "rsu_model", + "rsu_config", + "radar", + "rsu_log", + "camera", + "map", + "map_rsu", + "mng", + "rsi_event", + "rsm_participant", + "rsu_query", + "rsu_query_result", + "rsm", + "rsu_config_rsu", +] diff --git a/dandelion/crud/base.py b/dandelion/crud/base.py new file mode 100644 index 0000000..c0025bc --- /dev/null +++ b/dandelion/crud/base.py @@ -0,0 +1,78 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union + +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel +from sqlalchemy.orm import Session + +from dandelion.db.base_class import Base + +ModelType = TypeVar("ModelType", bound=Base) +CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) +UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) + + +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + def __init__(self, model: Type[ModelType]): + """ + CRUD object with default methods to Create, Read, Update, Delete (CRUD). + + **Parameters** + + * `model`: A SQLAlchemy model class + * `schema`: A Pydantic model (schema) class + """ + self.model = model + + def get(self, db: Session, id: Any) -> Optional[ModelType]: + return db.query(self.model).filter(self.model.id == id).first() + + def get_multi(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[ModelType]: + return db.query(self.model).offset(skip).limit(limit).all() + + def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType: + obj_in_data = jsonable_encoder(obj_in) + db_obj = self.model(**obj_in_data) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update( + self, db: Session, *, db_obj: ModelType, obj_in: Union[UpdateSchemaType, Dict[str, Any]] + ) -> ModelType: + obj_data = jsonable_encoder(db_obj) + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.dict(exclude_unset=True) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db_obj.update_time = datetime.utcnow() + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def remove(self, db: Session, *, id: int) -> ModelType: + obj = db.query(self.model).get(id) + db.delete(obj) + db.commit() + return obj diff --git a/dandelion/crud/crud_area.py b/dandelion/crud/crud_area.py new file mode 100644 index 0000000..fd94338 --- /dev/null +++ b/dandelion/crud/crud_area.py @@ -0,0 +1,41 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import Area +from dandelion.schemas import AreaCreate, AreaUpdate + + +class CRUDArea(CRUDBase[Area, AreaCreate, AreaUpdate]): + """""" + + def get_multi_by_city_code( + self, db: Session, city_code: str, *, skip: int = 0, limit: int = 100 + ) -> List[Area]: + return ( + db.query(self.model) + .filter(Area.city_code == city_code) + .offset(skip) + .limit(limit) + .all() + ) + + +area = CRUDArea(Area) diff --git a/dandelion/crud/crud_camera.py b/dandelion/crud/crud_camera.py new file mode 100644 index 0000000..37f65d6 --- /dev/null +++ b/dandelion/crud/crud_camera.py @@ -0,0 +1,57 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import Camera +from dandelion.schemas import CameraCreate, CameraUpdate + + +class CRUDCamera(CRUDBase[Camera, CameraCreate, CameraUpdate]): + """""" + + def create(self, db: Session, *, obj_in: CameraCreate) -> Camera: + obj_in_data = jsonable_encoder(obj_in, by_alias=False) + db_obj = self.model(**obj_in_data) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + sn: Optional[str] = None, + name: Optional[str] = None, + ) -> Tuple[int, List[Camera]]: + query_ = db.query(self.model) + if sn is not None: + query_ = query_.filter(self.model.sn.like(f"{sn}%")) + if name is not None: + query_ = query_.filter(self.model.name.like(f"{name}%")) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +camera = CRUDCamera(Camera) diff --git a/dandelion/crud/crud_city.py b/dandelion/crud/crud_city.py new file mode 100644 index 0000000..81c7465 --- /dev/null +++ b/dandelion/crud/crud_city.py @@ -0,0 +1,41 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import City +from dandelion.schemas import CityCreate, CityUpdate + + +class CRUDCity(CRUDBase[City, CityCreate, CityUpdate]): + """""" + + def get_multi_by_province_code( + self, db: Session, province_code: str, *, skip: int = 0, limit: int = 100 + ) -> List[City]: + return ( + db.query(self.model) + .filter(City.province_code == province_code) + .offset(skip) + .limit(limit) + .all() + ) + + +city = CRUDCity(City) diff --git a/dandelion/crud/crud_country.py b/dandelion/crud/crud_country.py new file mode 100644 index 0000000..415d533 --- /dev/null +++ b/dandelion/crud/crud_country.py @@ -0,0 +1,26 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dandelion.crud.base import CRUDBase +from dandelion.models import Country +from dandelion.schemas.country import CountryCreate, CountryUpdate + + +class CRUDCountry(CRUDBase[Country, CountryCreate, CountryUpdate]): + """""" + + +country = CRUDCountry(Country) diff --git a/dandelion/crud/crud_map.py b/dandelion/crud/crud_map.py new file mode 100644 index 0000000..09fc780 --- /dev/null +++ b/dandelion/crud/crud_map.py @@ -0,0 +1,48 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import Map +from dandelion.schemas import MapCreate, MapUpdate + + +class CRUDMap(CRUDBase[Map, MapCreate, MapUpdate]): + """""" + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + name: Optional[str] = None, + area_code: Optional[str] = None, + ) -> Tuple[int, List[Map]]: + query_ = db.query(self.model) + if name is not None: + query_ = query_.filter(self.model.name.like(f"{name}%")) + if area_code is not None: + query_ = query_.filter(self.model.area_code == area_code) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +map = CRUDMap(Map) diff --git a/dandelion/crud/crud_map_rsu.py b/dandelion/crud/crud_map_rsu.py new file mode 100644 index 0000000..1078523 --- /dev/null +++ b/dandelion/crud/crud_map_rsu.py @@ -0,0 +1,48 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import MapRSU +from dandelion.schemas import MapRSUCreate, MapRSUUpdate + + +class CRUDMapRSU(CRUDBase[MapRSU, MapRSUCreate, MapRSUUpdate]): + """""" + + def get_by_rsu_id(self, db: Session, *, rsu_id: int) -> MapRSU: + return db.query(self.model).filter(self.model.rsu_id == rsu_id).first() + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + map_id: Optional[int] = None, + ) -> Tuple[int, List[MapRSU]]: + query_ = db.query(self.model) + if map_id is not None: + query_ = query_.filter(self.model.map_id == map_id) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +map_rsu = CRUDMapRSU(MapRSU) diff --git a/dandelion/crud/crud_mng.py b/dandelion/crud/crud_mng.py new file mode 100644 index 0000000..e7edc79 --- /dev/null +++ b/dandelion/crud/crud_mng.py @@ -0,0 +1,47 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Optional + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import MNG +from dandelion.schemas import MNGCreate, MNGUpdate + + +class CRUDMNG(CRUDBase[MNG, MNGCreate, MNGUpdate]): + """""" + + def update_mng(self, db: Session, *, db_obj: MNG, obj_in: MNGUpdate) -> MNG: + obj_data = jsonable_encoder(db_obj) + update_data = jsonable_encoder(obj_in, by_alias=False) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db_obj.update_time = datetime.utcnow() + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_by_rsu_id(self, db: Session, *, rsu_id: int) -> Optional[MNG]: + return db.query(MNG).filter(MNG.rsu_id == rsu_id).first() + + +mng = CRUDMNG(MNG) diff --git a/dandelion/crud/crud_province.py b/dandelion/crud/crud_province.py new file mode 100644 index 0000000..a2ad875 --- /dev/null +++ b/dandelion/crud/crud_province.py @@ -0,0 +1,41 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import Province +from dandelion.schemas import ProvinceCreate, ProvinceUpdate + + +class CRUDProvince(CRUDBase[Province, ProvinceCreate, ProvinceUpdate]): + """""" + + def get_multi_by_country_code( + self, db: Session, country_code: str, *, skip: int = 0, limit: int = 100 + ) -> List[Province]: + return ( + db.query(self.model) + .filter(Province.country_code == country_code) + .offset(skip) + .limit(limit) + .all() + ) + + +province = CRUDProvince(Province) diff --git a/dandelion/crud/crud_radar.py b/dandelion/crud/crud_radar.py new file mode 100644 index 0000000..e63ad77 --- /dev/null +++ b/dandelion/crud/crud_radar.py @@ -0,0 +1,60 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import Radar +from dandelion.schemas import RadarCreate, RadarUpdate + + +class CRUDRadar(CRUDBase[Radar, RadarCreate, RadarUpdate]): + """""" + + def create(self, db: Session, *, obj_in: RadarCreate) -> Radar: + obj_in_data = jsonable_encoder(obj_in, by_alias=False) + db_obj = self.model(**obj_in_data) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + sn: Optional[str] = None, + name: Optional[str] = None, + rsu_id: Optional[int] = None, + ) -> Tuple[int, List[Radar]]: + query_ = db.query(self.model) + if sn is not None: + query_ = query_.filter(self.model.sn.like(f"{sn}%")) + if name is not None: + query_ = query_.filter(self.model.name.like(f"{name}%")) + if rsu_id is not None: + query_ = query_.filter(self.model.rsu_id == rsu_id) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +radar = CRUDRadar(Radar) diff --git a/dandelion/crud/crud_rsi_event.py b/dandelion/crud/crud_rsi_event.py new file mode 100644 index 0000000..a4f37d1 --- /dev/null +++ b/dandelion/crud/crud_rsi_event.py @@ -0,0 +1,61 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSU, RSIEvent +from dandelion.schemas import RSIEventCreate, RSIEventUpdate + + +class CRUDRSIEvent(CRUDBase[RSIEvent, RSIEventCreate, RSIEventUpdate]): + """""" + + def create_rsi_event(self, db: Session, *, obj_in: RSIEventCreate, rsu: RSU) -> RSIEvent: + obj_in_data = jsonable_encoder(obj_in, by_alias=False) + db_obj = self.model(**obj_in_data) + db_obj.rsu = rsu + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + event_type: Optional[int] = None, + area_code: Optional[str] = None, + address: Optional[str] = None, + ) -> Tuple[int, List[RSIEvent]]: + query_ = db.query(self.model) + if event_type is not None: + query_ = query_.filter(self.model.event_type == event_type) + if area_code is not None: + query_ = query_.filter(self.model.area_code == area_code) + if address is not None: + query_ = query_.filter(self.model.address.like(f"{address}%")) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +rsi_event = CRUDRSIEvent(RSIEvent) diff --git a/dandelion/crud/crud_rsm.py b/dandelion/crud/crud_rsm.py new file mode 100644 index 0000000..f1c8196 --- /dev/null +++ b/dandelion/crud/crud_rsm.py @@ -0,0 +1,42 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSM, Participants +from dandelion.schemas import RSMCreate, RSMUpdate + + +class CRUDRSM(CRUDBase[RSM, RSMCreate, RSMUpdate]): + """""" + + def create_rsm( + self, db: Session, *, obj_in: RSMCreate, participants: List[Participants] + ) -> RSM: + obj_in_data = jsonable_encoder(obj_in, by_alias=False) + db_obj = self.model(**obj_in_data) + db_obj.participants = participants + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + +rsm = CRUDRSM(RSM) diff --git a/dandelion/crud/crud_rsm_participant.py b/dandelion/crud/crud_rsm_participant.py new file mode 100644 index 0000000..9f6c645 --- /dev/null +++ b/dandelion/crud/crud_rsm_participant.py @@ -0,0 +1,45 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import Participants +from dandelion.schemas import RSMParticipantCreate, RSMParticipantUpdate + + +class CRUDRSMParticipant(CRUDBase[Participants, RSMParticipantCreate, RSMParticipantUpdate]): + """""" + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + ptc_type: Optional[int] = None, + ) -> Tuple[int, List[Participants]]: + query_ = db.query(self.model) + if ptc_type is not None: + query_ = query_.filter(self.model.ptc_type == ptc_type) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +rsm_participant = CRUDRSMParticipant(Participants) diff --git a/dandelion/crud/crud_rsu.py b/dandelion/crud/crud_rsu.py new file mode 100644 index 0000000..6bfadd8 --- /dev/null +++ b/dandelion/crud/crud_rsu.py @@ -0,0 +1,108 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import List, Optional, Tuple + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.crud.utils import get_mng_default +from dandelion.models import RSU +from dandelion.schemas import RSUCreate, RSUUpdate, RSUUpdateWithStatus, RSUUpdateWithVersion + + +class CRUDRSU(CRUDBase[RSU, RSUCreate, RSUUpdate]): + """""" + + def update_online_status( + self, db: Session, *, db_obj: RSU, obj_in: RSUUpdateWithStatus + ) -> RSU: + obj_data = jsonable_encoder(db_obj, by_alias=False) + update_data = obj_in.dict(exclude_unset=True) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db_obj.update_time = datetime.utcnow() + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update_with_version( + self, db: Session, *, db_obj: RSU, obj_in: RSUUpdateWithVersion + ) -> RSU: + obj_data = jsonable_encoder(db_obj, by_alias=False) + update_data = obj_in.dict(exclude_unset=True) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db_obj.update_time = datetime.utcnow() + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def create(self, db: Session, *, obj_in: RSUCreate) -> RSU: + obj_in_data = jsonable_encoder(obj_in, by_alias=False) + obj_in_data["version"] = "" + obj_in_data["location"] = {} + obj_in_data["config"] = {} + obj_in_data["rsu_status"] = True + obj_in_data["online_status"] = False + obj_in_data["mng"] = get_mng_default() + db_obj = self.model(**obj_in_data) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_first(self, db: Session) -> RSU: + return db.query(self.model).first() + + def get_by_rsu_esn(self, db: Session, *, rsu_esn: str) -> RSU: + return db.query(self.model).filter(self.model.rsu_esn == rsu_esn).first() + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + rsu_name: Optional[str] = None, + rsu_esn: Optional[str] = None, + area_code: Optional[str] = None, + online_status: Optional[bool] = None, + rsu_status: Optional[bool] = None, + ) -> Tuple[int, List[RSU]]: + query_ = db.query(self.model) + if rsu_name is not None: + query_ = query_.filter(self.model.rsu_name.like(f"{rsu_name}%")) + if rsu_esn is not None: + query_ = query_.filter(self.model.rsu_esn.like(f"{rsu_esn}%")) + if area_code is not None: + query_ = query_.filter(self.model.area_code == area_code) + if online_status is not None: + query_ = query_.filter(self.model.online_status == online_status) + if rsu_status is not None: + query_ = query_.filter(self.model.rsu_status == rsu_status) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +rsu = CRUDRSU(RSU) diff --git a/dandelion/crud/crud_rsu_config.py b/dandelion/crud/crud_rsu_config.py new file mode 100644 index 0000000..91f0398 --- /dev/null +++ b/dandelion/crud/crud_rsu_config.py @@ -0,0 +1,75 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import List, Optional, Tuple + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSU, RSUConfig, RSUConfigRSU +from dandelion.schemas import RSUConfigCreate, RSUConfigUpdate + + +class CRUDRSUConfig(CRUDBase[RSUConfig, RSUConfigCreate, RSUConfigUpdate]): + """""" + + def create_rsu_config( + self, db: Session, *, obj_in: RSUConfigCreate, rsus: List[RSU] + ) -> RSUConfig: + del obj_in.rsus + obj_in_data = jsonable_encoder(obj_in) + db_obj = self.model(**obj_in_data) + db_obj.rsus = [RSUConfigRSU.create(rsu_, db_obj) for rsu_ in rsus] + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update_rsu_config( + self, db: Session, *, db_obj: RSUConfig, obj_in: RSUConfigUpdate, rsus: List[RSU] + ) -> RSUConfig: + obj_data = jsonable_encoder(db_obj) + del obj_in.rsus + update_data = jsonable_encoder(obj_in, by_alias=True) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db_obj.rsus = [RSUConfigRSU.create(rsu_, db_obj) for rsu_ in rsus] + db_obj.update_time = datetime.utcnow() + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + name: Optional[str] = None, + ) -> Tuple[int, List[RSUConfig]]: + query_ = db.query(self.model) + if name is not None: + query_ = query_.filter(self.model.name.like(f"{name}%")) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +rsu_config = CRUDRSUConfig(RSUConfig) diff --git a/dandelion/crud/crud_rsu_config_rsu.py b/dandelion/crud/crud_rsu_config_rsu.py new file mode 100644 index 0000000..a3c64d7 --- /dev/null +++ b/dandelion/crud/crud_rsu_config_rsu.py @@ -0,0 +1,32 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSUConfigRSU +from dandelion.schemas import RSUConfigRSUCreate, RSUConfigRSUUpdate + + +class CRUDRSUConfigRSU(CRUDBase[RSUConfigRSU, RSUConfigRSUCreate, RSUConfigRSUUpdate]): + """""" + + def remove_by_rsu_config_id(self, db: Session, *, rsu_config_id: int) -> None: + db.query(self.model).filter(self.model.rsu_config_id == rsu_config_id).delete() + db.commit() + + +rsu_config_rsu = CRUDRSUConfigRSU(RSUConfigRSU) diff --git a/dandelion/crud/crud_rsu_log.py b/dandelion/crud/crud_rsu_log.py new file mode 100644 index 0000000..7b894b9 --- /dev/null +++ b/dandelion/crud/crud_rsu_log.py @@ -0,0 +1,75 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import List, Tuple + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSU, RSULog +from dandelion.schemas import RSULogCreate, RSULogUpdate + + +class CRUDRSULog(CRUDBase[RSULog, RSULogCreate, RSULogUpdate]): + """""" + + def create_rsu_log(self, db: Session, *, obj_in: RSULogCreate, rsus: List[RSU]) -> RSULog: + rsu_log = RSULog() + rsu_log.upload_url = obj_in.upload_url + rsu_log.user_id = obj_in.user_id + rsu_log.password = obj_in.password + rsu_log.transprotocal = obj_in.transprotocal + + obj_in_data = jsonable_encoder(rsu_log) + db_obj = self.model(**obj_in_data) + db_obj.rsus = rsus + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update_rsu_log( + self, db: Session, *, db_obj: RSULog, obj_in: RSULogUpdate, rsus: List[RSU] + ) -> RSULog: + obj_data = jsonable_encoder(db_obj) + del obj_in.rsus + update_data = obj_in.dict(exclude_unset=True) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db_obj.rsus = rsus + db_obj.update_time = datetime.utcnow() + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + ) -> Tuple[int, List[RSULog]]: + query_ = db.query(self.model) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +rsu_log = CRUDRSULog(RSULog) diff --git a/dandelion/crud/crud_rsu_model.py b/dandelion/crud/crud_rsu_model.py new file mode 100644 index 0000000..6eb529d --- /dev/null +++ b/dandelion/crud/crud_rsu_model.py @@ -0,0 +1,48 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSUModel +from dandelion.schemas import RSUModelCreate, RSUModelUpdate + + +class CRUDRSUModel(CRUDBase[RSUModel, RSUModelCreate, RSUModelUpdate]): + """""" + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + name: Optional[str] = None, + manufacturer: Optional[str] = None, + ) -> Tuple[int, List[RSUModel]]: + query_ = db.query(self.model) + if name is not None: + query_ = query_.filter(self.model.name.like(f"{name}%")) + if manufacturer is not None: + query_ = query_.filter(self.model.manufacturer.like(f"{manufacturer}%")) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +rsu_model = CRUDRSUModel(RSUModel) diff --git a/dandelion/crud/crud_rsu_query.py b/dandelion/crud/crud_rsu_query.py new file mode 100644 index 0000000..d802567 --- /dev/null +++ b/dandelion/crud/crud_rsu_query.py @@ -0,0 +1,51 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Tuple + +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSUQuery +from dandelion.schemas import RSUQueryCreate, RSUQueryUpdate + + +class CRUDRSUQuery(CRUDBase[RSUQuery, RSUQueryCreate, RSUQueryUpdate]): + """""" + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + ) -> Tuple[int, List[RSUQuery]]: + query_ = db.query(self.model) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + def create(self, db: Session, *, obj_in: RSUQueryCreate) -> RSUQuery: + db_obj = RSUQuery() + db_obj.query_type = obj_in.query_type + db_obj.time_type = obj_in.time_type + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + +rsu_query = CRUDRSUQuery(RSUQuery) diff --git a/dandelion/crud/crud_rsu_query_result.py b/dandelion/crud/crud_rsu_query_result.py new file mode 100644 index 0000000..de9ffb6 --- /dev/null +++ b/dandelion/crud/crud_rsu_query_result.py @@ -0,0 +1,26 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSUQueryResult +from dandelion.schemas import RSUQueryResultCreate, RSUQueryResultUpdate + + +class CRUDRSUQueryResult(CRUDBase[RSUQueryResult, RSUQueryResultCreate, RSUQueryResultUpdate]): + """""" + + +rsu_query_result = CRUDRSUQueryResult(RSUQueryResult) diff --git a/dandelion/crud/crud_rsu_tmp.py b/dandelion/crud/crud_rsu_tmp.py new file mode 100644 index 0000000..8433cae --- /dev/null +++ b/dandelion/crud/crud_rsu_tmp.py @@ -0,0 +1,57 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional, Tuple + +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from dandelion.crud.base import CRUDBase +from dandelion.models import RSUTMP +from dandelion.schemas import RSUTMPCreate, RSUTMPUpdate + + +class CRUDRSUTMP(CRUDBase[RSUTMP, RSUTMPCreate, RSUTMPUpdate]): + """""" + + def create(self, db: Session, *, obj_in: RSUTMPCreate) -> RSUTMP: + obj_in_data = jsonable_encoder(obj_in, by_alias=False) + db_obj = self.model(**obj_in_data) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_multi_with_total( + self, + db: Session, + *, + skip: int = 0, + limit: int = 10, + rsu_name: Optional[str] = None, + rsu_esn: Optional[str] = None, + ) -> Tuple[int, List[RSUTMP]]: + query_ = db.query(self.model) + if rsu_name is not None: + query_ = query_.filter(self.model.rsu_name.like(f"{rsu_name}%")) + if rsu_esn is not None: + query_ = query_.filter(self.model.rsu_esn == rsu_esn) + total = query_.count() + data = query_.offset(skip).limit(limit).all() + return total, data + + +rsu_tmp = CRUDRSUTMP(RSUTMP) diff --git a/dandelion/crud/crud_user.py b/dandelion/crud/crud_user.py new file mode 100644 index 0000000..7cc3787 --- /dev/null +++ b/dandelion/crud/crud_user.py @@ -0,0 +1,65 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Any, Dict, Optional, Union + +from sqlalchemy.orm import Session + +from dandelion.core.security import get_password_hash, verify_password +from dandelion.crud.base import CRUDBase +from dandelion.models import User +from dandelion.schemas import UserCreate, UserUpdate + + +class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): + def get_by_username(self, db: Session, *, username: str) -> Optional[User]: + return db.query(User).filter(User.username == username).first() + + def create(self, db: Session, *, obj_in: UserCreate) -> User: + db_obj = User() + db_obj.username = obj_in.username + db_obj.hashed_password = get_password_hash(obj_in.password) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update( + self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] + ) -> User: + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.dict(exclude_unset=True) + if update_data["password"]: + hashed_password = get_password_hash(update_data["password"]) + del update_data["password"] + update_data["hashed_password"] = hashed_password + return super().update(db, db_obj=db_obj, obj_in=update_data) + + def authenticate(self, db: Session, *, username: str, password: str) -> Optional[User]: + user = self.get_by_username(db, username=username) + if not user: + return None + if not verify_password(password, user.hashed_password): + return None + return user + + def is_active(self, user: User) -> bool: + return user.is_active + + +user = CRUDUser(User) diff --git a/dandelion/crud/utils.py b/dandelion/crud/utils.py new file mode 100644 index 0000000..9ace4dc --- /dev/null +++ b/dandelion/crud/utils.py @@ -0,0 +1,29 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dandelion.models import MNG +from dandelion.models.mng import Reboot + + +def get_mng_default() -> MNG: + mng = MNG() + mng.heartbeat_rate = 0 + mng.running_info_rate = 0 + mng.log_level = "NOLog" + mng.reboot = Reboot.not_reboot + mng.address_change = {"cssUrl": "", "time": 0} + mng.extend_config = "" + return mng diff --git a/dandelion/db/__init__.py b/dandelion/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/db/base.py b/dandelion/db/base.py new file mode 100644 index 0000000..4d0ef90 --- /dev/null +++ b/dandelion/db/base.py @@ -0,0 +1,43 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# flake8: noqa: F401 + +from __future__ import annotations + +# Import all the models, so that Base has them before being +# imported by Alembic +from dandelion.db.base_class import Base +from dandelion.models.area import Area +from dandelion.models.camera import Camera +from dandelion.models.city import City +from dandelion.models.country import Country +from dandelion.models.map import Map +from dandelion.models.map_rsu import MapRSU +from dandelion.models.mng import MNG +from dandelion.models.province import Province +from dandelion.models.radar import Radar +from dandelion.models.rsi_event import RSIEvent +from dandelion.models.rsm import RSM +from dandelion.models.rsm_participants import Participants +from dandelion.models.rsu import RSU +from dandelion.models.rsu_config import RSUConfig +from dandelion.models.rsu_config_rsu import RSUConfigRSU +from dandelion.models.rsu_log import RSULog +from dandelion.models.rsu_model import RSUModel +from dandelion.models.rsu_query import RSUQuery +from dandelion.models.rsu_query_result import RSUQueryResult +from dandelion.models.rsu_query_result_data import RSUQueryResultData +from dandelion.models.rsu_tmp import RSUTMP +from dandelion.models.user import User diff --git a/dandelion/db/base_class.py b/dandelion/db/base_class.py new file mode 100644 index 0000000..f3f9e4f --- /dev/null +++ b/dandelion/db/base_class.py @@ -0,0 +1,33 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime + +from sqlalchemy import Column, DateTime, Integer +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + + +class DandelionBase: + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + create_time = Column(DateTime, nullable=False, default=lambda: datetime.utcnow()) + update_time = Column( + DateTime, + nullable=False, + default=lambda: datetime.utcnow(), + onupdate=lambda: datetime.utcnow(), + ) diff --git a/dandelion/db/redis_pool.py b/dandelion/db/redis_pool.py new file mode 100644 index 0000000..73ca8bf --- /dev/null +++ b/dandelion/db/redis_pool.py @@ -0,0 +1,118 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import urllib +from logging import LoggerAdapter + +import redis +from oslo_config import cfg +from oslo_log import log +from oslo_utils import netutils, strutils +from redis import sentinel + +CONF: cfg = cfg.CONF +LOG: LoggerAdapter = log.getLogger(__name__) + +REDIS_CONN: redis.Redis + +CLIENT_ARGS = frozenset( + [ + "db", + "encoding", + "retry_on_timeout", + "socket_keepalive", + "socket_timeout", + "ssl", + "ssl_certfile", + "ssl_keyfile", + "sentinel", + "sentinel_fallback", + ] +) + +CLIENT_BOOL_ARGS = frozenset( + [ + "retry_on_timeout", + "ssl", + ] +) + +CLIENT_LIST_ARGS = frozenset( + [ + "sentinel_fallback", + ] +) + +CLIENT_INT_ARGS = frozenset( + [ + "db", + "socket_keepalive", + "socket_timeout", + ] +) + +DEFAULT_SOCKET_TIMEOUT: int = 30 + + +def setup_redis() -> None: + parsed_url = netutils.urlsplit(CONF.redis.connection) + options = urllib.parse.parse_qs(parsed_url.query) + + kwargs = {} + if parsed_url.hostname: + kwargs["host"] = parsed_url.hostname + if parsed_url.port: + kwargs["port"] = parsed_url.port + else: + if not parsed_url.path: + raise ValueError("Expected socket path in parsed urls path") + kwargs["unix_socket_path"] = parsed_url.path + if parsed_url.password: + kwargs["password"] = parsed_url.password + for a in CLIENT_ARGS: + if a not in options: + continue + if a in CLIENT_BOOL_ARGS: + v = strutils.bool_from_string(options[a][0]) + elif a in CLIENT_LIST_ARGS: + v = options[a] + elif a in CLIENT_INT_ARGS: + v = int(options[a][0]) + else: + v = options[a] + kwargs[a] = v + if "socket_timeout" not in kwargs: + kwargs["socket_timeout"] = DEFAULT_SOCKET_TIMEOUT + + # Ask the sentinel for the current master if there is a + # sentinel arg. + global REDIS_CONN + if "sentinel" in kwargs: + sentinel_hosts = [ + tuple(fallback.split(":")) for fallback in kwargs.get("sentinel_fallback", []) + ] + sentinel_hosts.insert(0, (kwargs["host"], kwargs["port"])) + sentinel_server = sentinel.Sentinel( + sentinel_hosts, socket_timeout=kwargs["socket_timeout"] + ) + sentinel_name = kwargs["sentinel"][0] + del kwargs["sentinel"] + if "sentinel_fallback" in kwargs: + del kwargs["sentinel_fallback"] + REDIS_CONN = sentinel_server.master_for(sentinel_name, **kwargs) + else: + REDIS_CONN = redis.StrictRedis(**kwargs) + LOG.info("Redis setup complete") diff --git a/dandelion/db/session.py b/dandelion/db/session.py new file mode 100644 index 0000000..38fca8b --- /dev/null +++ b/dandelion/db/session.py @@ -0,0 +1,44 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter + +from oslo_config import cfg +from oslo_log import log +from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker + +CONF = cfg.CONF +LOG: LoggerAdapter = log.getLogger(__name__) + + +DB_SESSION_LOCAL: Session + + +def setup_db() -> None: + if CONF.database.connection.startswith("sqlite"): + engine = create_engine( + CONF.database.connection, + pool_pre_ping=True, + connect_args={"check_same_thread": False}, + ) + else: + engine = create_engine(CONF.database.connection, pool_pre_ping=True) + + global DB_SESSION_LOCAL + DB_SESSION_LOCAL = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + LOG.info("DB setup complete") diff --git a/dandelion/main.py b/dandelion/main.py new file mode 100644 index 0000000..dd33a79 --- /dev/null +++ b/dandelion/main.py @@ -0,0 +1,112 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time +import uuid +from logging import LoggerAdapter + +from fastapi import FastAPI, Request +from fastapi_utils.tasks import repeat_every +from oslo_config import cfg +from oslo_log import log +from starlette.middleware.cors import CORSMiddleware + +from dandelion import constants, periodic_tasks, version +from dandelion.api.api_v1.api import api_router +from dandelion.db import redis_pool, session as db_session +from dandelion.mqtt import server as mqtt_server + +CONF: cfg = cfg.CONF +LOG: LoggerAdapter = log.getLogger(__name__) + +app = FastAPI( + title="Dandelion - OpenV2X Device Management - APIServer", + openapi_url=f"{constants.API_V1_STR}/openapi.json", +) + + +# Startup +@app.on_event("startup") +def prepare() -> None: + log.register_options(CONF) + CONF( + args=["--config-file", constants.CONFIG_FILE_PATH], + project=constants.PROJECT_NAME, + version=version.version_string(), + ) + log.setup(CONF, constants.PROJECT_NAME) + + +@app.on_event("startup") +def setup_mqtt() -> None: + mqtt_server.connect() + + +@app.on_event("startup") +def setup_db() -> None: + db_session.setup_db() + + +@app.on_event("startup") +def setup_redis() -> None: + redis_pool.setup_redis() + + +@app.on_event("startup") +def setup_app(): + # Set all CORS enabled origins + if CONF.cors.origins: + app.add_middleware( + CORSMiddleware, + allow_origins=CONF.cors.origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + +@app.on_event("startup") +@repeat_every(seconds=60) +def update_rsu_online_status() -> None: + periodic_tasks.update_rsu_online_status() + + +# Shutdown +@app.on_event("shutdown") +def shutdown_event(): + LOG.info("Shutting down...") + + +# Middleware +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + + +@app.middleware("http") +async def add_request_id_header(request: Request, call_next): + request_id = uuid.uuid4().hex + LOG.info(f"Request path: {request.url.path}, request id: {request_id}") + response = await call_next(request) + response.headers["OpenV2X-Request-ID"] = request_id + return response + + +app.include_router(api_router, prefix=constants.API_V1_STR) diff --git a/dandelion/models/__init__.py b/dandelion/models/__init__.py new file mode 100644 index 0000000..273adfb --- /dev/null +++ b/dandelion/models/__init__.py @@ -0,0 +1,40 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# flake8: noqa: F401 + +from __future__ import annotations + +from .area import Area +from .camera import Camera +from .city import City +from .country import Country +from .map import Map +from .map_rsu import MapRSU +from .mng import MNG +from .province import Province +from .radar import Radar +from .rsi_event import RSIEvent +from .rsm import RSM +from .rsm_participants import Participants +from .rsu import RSU +from .rsu_config import RSUConfig +from .rsu_config_rsu import RSUConfigRSU +from .rsu_log import RSULog +from .rsu_model import RSUModel +from .rsu_query import RSUQuery +from .rsu_query_result import RSUQueryResult +from .rsu_query_result_data import RSUQueryResultData +from .rsu_tmp import RSUTMP +from .user import User diff --git a/dandelion/models/area.py b/dandelion/models/area.py new file mode 100644 index 0000000..bac5af3 --- /dev/null +++ b/dandelion/models/area.py @@ -0,0 +1,65 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Column, ForeignKey, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.util import Optional as Optional_util + + +class Area(Base, DandelionBase): + __tablename__ = "area" + + city_code = Column(String(64), ForeignKey("city.code")) + code = Column(String(64), unique=True, index=True, nullable=False) + name = Column(String(64), nullable=False) + + maps = relationship("Map", backref="area") + rsus = relationship("RSU", backref="area") + rsi_events = relationship("RSIEvent", backref="area") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict(code=self.code, name=self.name) + + def to_all(self): + return dict( + countryCode=Optional_util.none(self.city) + .map(lambda v: v.province) + .map(lambda v: v.country) + .map(lambda v: v.code) + .get(), + countryName=Optional_util.none(self.city) + .map(lambda v: v.province) + .map(lambda v: v.country) + .map(lambda v: v.name) + .get(), + provinceCode=Optional_util.none(self.city) + .map(lambda v: v.province) + .map(lambda v: v.code) + .get(), + provinceName=Optional_util.none(self.city) + .map(lambda v: v.province) + .map(lambda v: v.name) + .get(), + cityCode=Optional_util.none(self.city).map(lambda v: v.code).get(), + cityName=Optional_util.none(self.city).map(lambda v: v.name).get(), + areaCode=self.code, + areaName=self.name, + ) diff --git a/dandelion/models/camera.py b/dandelion/models/camera.py new file mode 100644 index 0000000..0837de6 --- /dev/null +++ b/dandelion/models/camera.py @@ -0,0 +1,57 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.util import Optional as Optional_util + + +class Camera(Base, DandelionBase): + __tablename__ = "camera" + + sn = Column(String(64), nullable=False, index=True, unique=True) + name = Column(String(64), nullable=False, index=True, default="") + stream_url = Column(String(255), nullable=False, default="") + lng = Column(Float, nullable=False) + lat = Column(Float, nullable=False) + elevation = Column(Float, nullable=False) + towards = Column(Float, nullable=False) + status = Column(Boolean, nullable=False, default=True) + rsu_id = Column(Integer, ForeignKey("rsu.id")) + desc = Column(String(255), nullable=False, default="") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return { + **dict( + id=self.id, + sn=self.sn, + name=self.name, + streamUrl=self.stream_url, + lng=self.lng, + lat=self.lat, + elevation=self.elevation, + towards=self.towards, + rsuId=self.rsu_id, + rsuName=Optional_util.none(self.rsu).map(lambda v: v.rsu_name).get(), + desc=self.desc, + createTime=self.create_time, + ), + **Optional_util.none(self.rsu).map(lambda v: v.area).map(lambda v: v.to_all()).get(), + } diff --git a/dandelion/models/city.py b/dandelion/models/city.py new file mode 100644 index 0000000..3d1e34d --- /dev/null +++ b/dandelion/models/city.py @@ -0,0 +1,42 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List + +from sqlalchemy import Column, ForeignKey, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.models.area import Area + + +class City(Base, DandelionBase): + __tablename__ = "city" + + province_code = Column(String(64), ForeignKey("province.code")) + code = Column(type_=String(64), unique=True, index=True, nullable=False) + name = Column(type_=String(64), nullable=False) + + areas: List[Area] = relationship("Area", backref="city") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict(code=self.code, name=self.name) + + def to_all_dict(self): + return {**self.to_dict(), "children": [v.to_dict() for v in self.areas]} diff --git a/dandelion/models/country.py b/dandelion/models/country.py new file mode 100644 index 0000000..384d7c5 --- /dev/null +++ b/dandelion/models/country.py @@ -0,0 +1,41 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List + +from sqlalchemy import Column, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.models.province import Province + + +class Country(Base, DandelionBase): + __tablename__ = "country" + + code = Column(type_=String(64), unique=True, index=True, nullable=False) + name = Column(type_=String(64), nullable=False) + + provinces: List[Province] = relationship("Province", backref="country") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict(code=self.code, name=self.name) + + def to_all_dict(self): + return {**self.to_dict(), "children": [v.to_all_dict() for v in self.provinces]} diff --git a/dandelion/models/map.py b/dandelion/models/map.py new file mode 100644 index 0000000..4245d41 --- /dev/null +++ b/dandelion/models/map.py @@ -0,0 +1,53 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import JSON, Column, Float, ForeignKey, String +from sqlalchemy.orm import deferred, relationship + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.util import Optional as Optional_util + + +class Map(Base, DandelionBase): + __tablename__ = "map" + + name = Column(String(64), nullable=False, index=True, unique=True) + address = Column(String(255), nullable=False, default="", comment="specific location") + area_code = Column(String(64), ForeignKey("area.code")) + desc = Column(String(255), nullable=False, default="") + lat = Column(Float, nullable=False) + lng = Column(Float, nullable=False) + data = deferred(Column(JSON, nullable=True)) + + rsus = relationship("MapRSU", backref="map") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return { + **dict( + id=self.id, + name=self.name, + address=self.address, + desc=self.desc, + amount=len(self.rsus), + lat=self.lat, + lng=self.lng, + createTime=self.create_time, + ), + **Optional_util.none(self.area).map(lambda v: v.to_all()).orElse({}), + } diff --git a/dandelion/models/map_rsu.py b/dandelion/models/map_rsu.py new file mode 100644 index 0000000..c1d97be --- /dev/null +++ b/dandelion/models/map_rsu.py @@ -0,0 +1,46 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.util import Optional as Optional_util + + +class MapRSU(Base, DandelionBase): + __tablename__ = "map_rsu" + + map_id = Column(Integer, ForeignKey("map.id")) + rsu_id = Column(Integer, ForeignKey("rsu.id")) + # TODO 下发状态 0=下发中 1=下发成功 2=下发失败 + status = Column(type_=Integer, nullable=False, default=0) + + rsu = relationship("RSU") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict( + id=self.id, + rsuName=Optional_util.none(self.rsu).map(lambda v: v.rsu_name).get(), + rsuSn=Optional_util.none(self.rsu).map(lambda v: v.rsu_esn).get(), + onlineStatus=Optional_util.none(self.rsu).map(lambda v: v.online_status).get(), + rsuStatus=Optional_util.none(self.rsu).map(lambda v: v.rsu_status).get(), + deliveryStatus=self.status, + createTime=self.create_time, + ) diff --git a/dandelion/models/mng.py b/dandelion/models/mng.py new file mode 100644 index 0000000..96062e5 --- /dev/null +++ b/dandelion/models/mng.py @@ -0,0 +1,76 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import enum + +from sqlalchemy import JSON, Column, Enum, ForeignKey, Integer, String +from sqlalchemy.orm import backref, relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class Reboot(enum.Enum): + # TODO 0: 不重启, 1: 重启 + not_reboot = 0 + reboot = 1 + + +class MNG(Base, DandelionBase): + __tablename__ = "mng" + + rsu_id = Column(Integer, ForeignKey("rsu.id")) + # TODO 心跳上报频率 0: 不上报心跳信息 >0: 表示上报间隔,秒 + heartbeat_rate = Column(Integer, nullable=True, default=60) + # TODO 设备运行状态上报频率 0: 不上报设备运行状态信息 >0: 表示上报间隔,秒 + running_info_rate = Column(Integer, nullable=True, default=60) + # TODO 日志上报频率 0: 不上报日志信息 >0: 表示上报间隔,秒 + log_rate = Column(Integer, nullable=True, default=0) + log_level = Column(Enum("DEBUG", "INFO", "ERROR", "WARN", "NOLog"), nullable=True) + reboot = Column(Enum(Reboot), nullable=True) + address_change = Column(JSON, nullable=True) + extend_config = Column(String(64), nullable=False) + + rsu = relationship("RSU", backref=backref("mng", uselist=False)) + + def __repr__(self) -> str: + return f"" + + def all_dict(self): + return dict( + id=self.id, + rsuName=self.rsu.rsu_name, + rsuEsn=self.rsu.rsu_esn, + hbRate=self.heartbeat_rate, + runningInfoRate=self.running_info_rate, + addressChg=self.address_change, + logLevel=self.log_level, + reboot=self.reboot.name, + extendConfig=self.extend_config, + createTime=self.create_time, + ) + + def mqtt_dict(self): + return dict( + rsuName=self.rsu.rsu_name, + rsuEsn=self.rsu.rsu_esn, + protocolVersion=self.rsu.version, + hbRate=self.heartbeat_rate, + runningInfoRate=self.running_info_rate, + addressChg=self.address_change, + logLevel=self.log_level, + reboot=self.reboot.value, + extendConfig=self.extend_config, + ) diff --git a/dandelion/models/province.py b/dandelion/models/province.py new file mode 100644 index 0000000..fa5874b --- /dev/null +++ b/dandelion/models/province.py @@ -0,0 +1,42 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List + +from sqlalchemy import Column, ForeignKey, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.models import City + + +class Province(Base, DandelionBase): + __tablename__ = "province" + + country_code = Column(String(64), ForeignKey("country.code")) + code = Column(String(64), unique=True, index=True, nullable=False) + name = Column(String(64), nullable=False) + + cities: List[City] = relationship("City", backref="province") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict(code=self.code, name=self.name) + + def to_all_dict(self): + return {**self.to_dict(), "children": [v.to_all_dict() for v in self.cities]} diff --git a/dandelion/models/radar.py b/dandelion/models/radar.py new file mode 100644 index 0000000..a885efd --- /dev/null +++ b/dandelion/models/radar.py @@ -0,0 +1,57 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.util import Optional as Optional_util + + +class Radar(Base, DandelionBase): + __tablename__ = "radar" + + sn = Column(String(64), nullable=False, index=True, unique=True) + name = Column(String(64), nullable=False, index=True, default="") + radar_ip = Column(String(15), nullable=False, default="") + lng = Column(Float, nullable=False) + lat = Column(Float, nullable=False) + elevation = Column(Float, nullable=False) + towards = Column(Float, nullable=False) + status = Column(Boolean, nullable=False, default=True) + rsu_id = Column(Integer, ForeignKey("rsu.id")) + desc = Column(String(255), nullable=False, default="") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return { + **dict( + id=self.id, + sn=self.sn, + name=self.name, + radarIP=self.radar_ip, + lng=self.lng, + lat=self.lat, + elevation=self.elevation, + towards=self.towards, + rsuId=self.rsu_id, + rsuName=Optional_util.none(self.rsu).map(lambda v: v.rsu_name).get(), + desc=self.desc, + createTime=self.create_time, + ), + **Optional_util.none(self.rsu).map(lambda v: v.area).map(lambda v: v.to_all()).get(), + } diff --git a/dandelion/models/rsi_event.py b/dandelion/models/rsi_event.py new file mode 100644 index 0000000..b3efce4 --- /dev/null +++ b/dandelion/models/rsi_event.py @@ -0,0 +1,101 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import enum + +from sqlalchemy import JSON, Boolean, Column, Enum, Float, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.util import Optional as Optional_util + + +class EventClass(enum.Enum): + # TODO 异常路况 + AbnormalTraffic = 1 + # TODO 恶劣天气 + AdverseWeather = 2 + # TODO 异常车况 + AbnormalVehicle = 3 + # TODO 标志标牌 + TrafficSign = 4 + + +class EventSource(enum.Enum): + unknown = 1 + police = 2 + government = 3 + meteorological = 4 + internet = 5 + detection = 6 + + +class RSIEvent(Base, DandelionBase): + __tablename__ = "rsi_event" + + rsu_id = Column(Integer, ForeignKey("rsu.id")) + area_code = Column(String(64), ForeignKey("area.code")) + address = Column(String(64), nullable=False, index=True, default="") + alert_id = Column(String(64), nullable=True, default="") + duration = Column(Integer, nullable=True, default=0) + event_status = Column(Boolean, nullable=True, default=True) + timestamp = Column(String(64), nullable=True, default="", comment="yyyy-MM-ddT HH:mm:ss.SSS Z") + event_class = Column(Enum(EventClass), nullable=True) + event_type = Column(Integer, nullable=True, default="") + event_source = Column(Enum(EventSource), nullable=True) + event_confidence = Column(Float, nullable=True, default=0) + event_position = Column(JSON, nullable=True) + event_radius = Column(Float, nullable=True, default=0) + event_description = Column(String(255), nullable=True, default="") + event_priority = Column(Integer, nullable=True) + reference_paths = Column(JSON, nullable=True) + + rsu = relationship("RSU", backref="rsi_events") + + def __repr__(self) -> str: + return f"" + + def to_all_dict(self): + return { + **self.to_dict(), + **dict( + alertID=self.alert_id, + duration=self.duration, + eventStatus=self.event_status, + timestamp=self.timestamp, + eventSource=self.event_source.name, + eventConfidence=self.event_confidence, + eventPosition=self.event_position, + eventRadius=self.event_radius, + eventDescription=self.event_description, + eventPriority=self.event_priority, + reference_paths=self.reference_paths, + ), + } + + def to_dict(self): + return { + **dict( + id=self.id, + rsuName=Optional_util.none(self.rsu).map(lambda v: v.rsu_name).get(), + rsuEsn=Optional_util.none(self.rsu).map(lambda v: v.rsu_esn).get(), + address=self.address, + eventClass=self.event_class.name, + eventType=self.event_type, + createTime=self.create_time, + ), + **self.area.to_all(), + } diff --git a/dandelion/models/rsm.py b/dandelion/models/rsm.py new file mode 100644 index 0000000..ab705ac --- /dev/null +++ b/dandelion/models/rsm.py @@ -0,0 +1,30 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import JSON, Column +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class RSM(Base, DandelionBase): + __tablename__ = "rsm" + + ref_pos = Column(JSON, nullable=False) + participants = relationship("Participants", backref="rsm") + + def __repr__(self) -> str: + return f"" diff --git a/dandelion/models/rsm_participants.py b/dandelion/models/rsm_participants.py new file mode 100644 index 0000000..f08b14d --- /dev/null +++ b/dandelion/models/rsm_participants.py @@ -0,0 +1,67 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from enum import Enum + +from sqlalchemy import JSON, Column, Enum as db_Enum, ForeignKey, Integer, String + +from dandelion.db.base_class import Base, DandelionBase +from dandelion.util import Optional as Optional_util + + +class PtcType(Enum): + # TODO + unknown = "未知类型" + motor = "机动车" + non_motor = "非机动车" + pedestrian = "行人" + rsu = "RSU设备" + + +class Participants(Base, DandelionBase): + __tablename__ = "rsm_participants" + + rsm_id = Column(Integer, ForeignKey("rsm.id")) + + ptc_type = Column(db_Enum(PtcType), nullable=False) + ptc_id = Column(Integer, nullable=False) + source = Column(Integer, nullable=False) + sec_mark = Column(Integer, nullable=True) + pos = Column(JSON, nullable=False) + accuracy = Column(String(255), nullable=True) + speed = Column(Integer, nullable=True) + heading = Column(Integer, nullable=True) + size = Column(JSON, nullable=True) + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict( + id=self.id, + ptcId=self.ptc_id, + ptcType=self.ptc_type.name, + ptcTypeName=self.ptc_type.value, + source=self.source, + secMark=self.sec_mark, + lon=Optional_util.none(self.pos).map(lambda v: v.get("lon")).get(), + lat=Optional_util.none(self.pos).map(lambda v: v.get("lat")).get(), + accuracy=self.accuracy, + speed=self.speed, + heading=self.heading, + size=self.size, + createTime=self.create_time, + ) diff --git a/dandelion/models/rsu.py b/dandelion/models/rsu.py new file mode 100644 index 0000000..5e787f8 --- /dev/null +++ b/dandelion/models/rsu.py @@ -0,0 +1,84 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class RSU(Base, DandelionBase): + __tablename__ = "rsu" + + rsu_id = Column(String(64), nullable=False) + rsu_esn = Column(String(64), nullable=False, index=True, unique=True, comment="serial number") + rsu_ip = Column(String(64), nullable=False) + rsu_name = Column(String(64), index=True, nullable=False) + version = Column(String(64), nullable=False) + rsu_status = Column(Boolean, index=True, nullable=False, default=True) + location = Column(JSON, nullable=False) + config = Column(JSON, nullable=False) + online_status = Column(Boolean, index=True, nullable=False, default=False) + rsu_model_id = Column(Integer, ForeignKey("rsu_model.id")) + area_code = Column(String(64), ForeignKey("area.code")) + address = Column( + String(255), nullable=False, default="", comment="Installation specific location" + ) + desc = Column(String(255), nullable=True, default="") + log_id = Column(Integer, ForeignKey("rsu_log.id")) + + cameras = relationship("Camera", backref="rsu") + radars = relationship("Radar", backref="rsu") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict( + id=self.id, + rsuId=self.rsu_id, + rsuEsn=self.rsu_esn, + rsuName=self.rsu_name, + rsuIP=self.rsu_ip, + version=self.version, + rsuStatus=self.rsu_status, + onlineStatus=self.online_status, + rsuModelId=self.rsu_model_id, + areaCode=self.area_code, + address=self.address, + desc=self.desc, + location=self.location, + config=self.config, + createTime=self.create_time, + updateTime=self.update_time, + ) + + def to_all_dict(self): + return {**self.to_dict(), **self.area.to_all()} + + def to_info_dict(self): + return {**self.to_all_dict(), "config": self.rsu_config_rsu} + + def mqtt_dict(self): + return dict( + rsuId=self.rsu_id, + rsuEsn=self.rsu_esn, + rsuName=self.rsu_name, + version=self.version, + rsuStatus=self.rsu_status, + location=self.location, + config=self.config, + ) diff --git a/dandelion/models/rsu_config.py b/dandelion/models/rsu_config.py new file mode 100644 index 0000000..3454e0e --- /dev/null +++ b/dandelion/models/rsu_config.py @@ -0,0 +1,51 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import JSON, Column, String + +from dandelion.db.base_class import Base, DandelionBase + + +class RSUConfig(Base, DandelionBase): + __tablename__ = "rsu_config" + + name = Column(String(64), nullable=False, index=True, unique=True) + bsm = Column(JSON, nullable=True) + rsi = Column(JSON, nullable=True) + rsm = Column(JSON, nullable=True) + map = Column(JSON, nullable=True) + spat = Column(JSON, nullable=True) + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return { + **self.mqtt_dict(), + **dict(id=self.id, name=self.name, createTime=self.create_time), + } + + def to_all_dict(self): + return {**self.to_dict(), **dict(rsus=[v.rsu.to_all_dict() for v in self.rsu_config_rsu])} + + def mqtt_dict(self): + return dict( + bsmConfig=self.bsm, + rsiConfig=self.rsi, + spatConfig=self.spat, + rsmConfig=self.rsm, + mapConfig=self.map, + ) diff --git a/dandelion/models/rsu_config_rsu.py b/dandelion/models/rsu_config_rsu.py new file mode 100644 index 0000000..8de28b6 --- /dev/null +++ b/dandelion/models/rsu_config_rsu.py @@ -0,0 +1,51 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class RSUConfigRSU(Base, DandelionBase): + __tablename__ = "rsu_config_rsu" + + rsu_config_id = Column(Integer, ForeignKey("rsu_config.id")) + rsu_id = Column(Integer, ForeignKey("rsu.id")) + # TODO 下发状态 0=下发中 1=下发成功 2=下发失败 + status = Column(Integer, nullable=False, default=0) + + rsu = relationship("RSU", backref="rsu_config_rsu") + rsu_config = relationship("RSUConfig", backref="rsu_config_rsu") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict( + id=self.id, + rsu_id=self.rsu_id, + rsu_config_id=self.rsu_config_id, + status=self.status, + create_time=self.create_time, + ) + + @staticmethod + def create(rsu, rsu_config): + config = RSUConfigRSU() + config.rsu = rsu + config.rsu_config = rsu_config + return config diff --git a/dandelion/models/rsu_log.py b/dandelion/models/rsu_log.py new file mode 100644 index 0000000..d90848b --- /dev/null +++ b/dandelion/models/rsu_log.py @@ -0,0 +1,53 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Column, Enum, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class RSULog(Base, DandelionBase): + __tablename__ = "rsu_log" + + upload_url = Column(String(64), nullable=False) + user_id = Column(String(64), nullable=False) + password = Column(String(255), nullable=False) + transprotocal = Column(Enum("http", "https", "ftp", "sftp", "other"), nullable=False) + + rsus = relationship("RSU", backref="rsu_log") + + def __repr__(self) -> str: + return f"" + + def to_all_dict(self): + return dict( + id=self.id, + uploadUrl=self.upload_url, + userId=self.user_id, + password=self.password, + transprotocal=self.transprotocal, + createTime=self.create_time, + rsus=[dict(id=rsu.id, rsuName=rsu.rsu_name, rsuEsn=rsu.rsu_esn) for rsu in self.rsus], + ) + + def mqtt_dict(self): + return dict( + uploadUrl=self.upload_url, + userId=self.user_id, + password=self.password, + transprotocal=self.transprotocal, + ) diff --git a/dandelion/models/rsu_model.py b/dandelion/models/rsu_model.py new file mode 100644 index 0000000..ec4ac9c --- /dev/null +++ b/dandelion/models/rsu_model.py @@ -0,0 +1,42 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Column, String +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class RSUModel(Base, DandelionBase): + __tablename__ = "rsu_model" + + name = Column(String(64), nullable=False, index=True, unique=True) + manufacturer = Column(String(64), index=True, nullable=False) + desc = Column(String(255), nullable=False) + + rsus = relationship("RSU", backref="rsu_model") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict( + id=self.id, + name=self.name, + manufacturer=self.manufacturer, + desc=self.desc, + createTime=self.create_time, + ) diff --git a/dandelion/models/rsu_query.py b/dandelion/models/rsu_query.py new file mode 100644 index 0000000..508450f --- /dev/null +++ b/dandelion/models/rsu_query.py @@ -0,0 +1,51 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Column, Integer +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class TimeType(object): + one_hour = 1 + one_day = 2 + one_week = 3 + # TODO 开机至今 + to_date = 4 + + +class RSUQuery(Base, DandelionBase): + __tablename__ = "rsu_query" + + query_type = Column(Integer, nullable=False) + time_type = Column(Integer) + results = relationship("RSUQueryResult", backref="query") + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict( + id=self.id, + queryType=self.query_type, + timeType=self.time_type, + createTime=self.create_time, + rsus=self.result_dict(), + ) + + def result_dict(self): + return [v.rsu_dict() for v in self.results] diff --git a/dandelion/models/rsu_query_result.py b/dandelion/models/rsu_query_result.py new file mode 100644 index 0000000..07386f0 --- /dev/null +++ b/dandelion/models/rsu_query_result.py @@ -0,0 +1,48 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.orm import relationship + +from dandelion.db.base_class import Base, DandelionBase + + +class RSUQueryResult(Base, DandelionBase): + __tablename__ = "rsu_query_result" + + query_id = Column(Integer, ForeignKey("rsu_query.id")) + rsu_id = Column(Integer, ForeignKey("rsu.id")) + rsu = relationship("RSU") + data = relationship("RSUQueryResultData", backref="result") + + def __repr__(self) -> str: + return f"" + + def to_all_dict(self): + return {**self.to_dict(), **dict(data=[v.data for v in self.data])} + + def to_dict(self): + return { + **dict( + queryType=self.query.query_type, + timeType=self.query.time_type, + createTime=self.create_time, + ), + **self.rsu_dict(), + } + + def rsu_dict(self): + return dict(rsuId=self.rsu.id, rsuName=self.rsu.rsu_name, rsuEsn=self.rsu.rsu_esn) diff --git a/dandelion/models/rsu_query_result_data.py b/dandelion/models/rsu_query_result_data.py new file mode 100644 index 0000000..a1c3fca --- /dev/null +++ b/dandelion/models/rsu_query_result_data.py @@ -0,0 +1,29 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import JSON, Column, ForeignKey, Integer + +from dandelion.db.base_class import Base, DandelionBase + + +class RSUQueryResultData(Base, DandelionBase): + __tablename__ = "rsu_query_result_data" + + result_id = Column(Integer, ForeignKey("rsu_query_result.id")) + data = Column(JSON, nullable=True) + + def __repr__(self) -> str: + return f"" diff --git a/dandelion/models/rsu_tmp.py b/dandelion/models/rsu_tmp.py new file mode 100644 index 0000000..07f7647 --- /dev/null +++ b/dandelion/models/rsu_tmp.py @@ -0,0 +1,44 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import JSON, Column, String + +from dandelion.db.base_class import Base, DandelionBase + + +class RSUTMP(Base, DandelionBase): + __tablename__ = "rsu_tmp" + + rsu_id = Column(String(64), nullable=False) + rsu_esn = Column(String(64), nullable=False, index=True, unique=True) + rsu_name = Column(String(64), nullable=False, index=True) + rsu_status = Column(String(64), nullable=False) + version = Column(String(64), nullable=False) + location = Column(JSON, nullable=False) + config = Column(JSON, nullable=False) + + def __repr__(self) -> str: + return f"" + + def to_dict(self): + return dict( + id=self.id, + rsuId=self.rsu_id, + rsuName=self.rsu_name, + rsuEsn=self.rsu_esn, + version=self.version, + createTime=self.create_time, + ) diff --git a/dandelion/models/user.py b/dandelion/models/user.py new file mode 100755 index 0000000..b21822e --- /dev/null +++ b/dandelion/models/user.py @@ -0,0 +1,31 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from sqlalchemy import Boolean, Column, String + +from dandelion.db.base_class import Base, DandelionBase + + +class User(Base, DandelionBase): + __tablename__ = "user" + + username = Column(String(length=255), unique=True, index=True, nullable=False) + hashed_password = Column(String(length=4096), nullable=False) + is_active = Column(Boolean(), default=True) + is_superuser = Column(Boolean(), default=False) + + def __repr__(self) -> str: + return f"" diff --git a/dandelion/mqtt/__init__.py b/dandelion/mqtt/__init__.py new file mode 100644 index 0000000..48e99a5 --- /dev/null +++ b/dandelion/mqtt/__init__.py @@ -0,0 +1,33 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log + +from dandelion.mqtt import server + +LOG: LoggerAdapter = log.getLogger(__name__) + + +def send_msg(topic: str, msg: Dict[str, Any]) -> None: + LOG.debug(f"Start to send message [{msg}] to topic [{topic}]") + client: mqtt.Client = server.GET_MQTT_CLIENT() + client.publish(topic=topic, payload=json.dumps(msg), qos=0) + LOG.info(f"Message [{msg}] has been sent to topic [{topic}]") diff --git a/dandelion/mqtt/error/__init__.py b/dandelion/mqtt/error/__init__.py new file mode 100644 index 0000000..b605067 --- /dev/null +++ b/dandelion/mqtt/error/__init__.py @@ -0,0 +1,29 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +class MQTTError(Exception): + def __init__(self, code: int, msg: str): + self.code = code + self.msg = msg + + @staticmethod + def param(msg_: str) -> "MQTTError": + return MQTTError(1, msg_) + + @staticmethod + def system(msg_: str) -> "MQTTError": + return MQTTError(2, msg_) diff --git a/dandelion/mqtt/server.py b/dandelion/mqtt/server.py new file mode 100644 index 0000000..febf452 --- /dev/null +++ b/dandelion/mqtt/server.py @@ -0,0 +1,91 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import uuid +from logging import LoggerAdapter +from typing import Any, Callable, Dict + +import paho.mqtt.client as mqtt +from oslo_config import cfg +from oslo_log import log + +from dandelion import conf +from dandelion.mqtt.service import RouterHandler +from dandelion.mqtt.service.algorithm.rsi import RSIRouterHandler +from dandelion.mqtt.service.algorithm.rsm import RSMRouterHandler +from dandelion.mqtt.service.map.map_up import MapRouterHandler +from dandelion.mqtt.service.rsu.rsu_base_info import RSUBaseINFORouterHandler +from dandelion.mqtt.service.rsu.rsu_heartbeat import RSUHeartbeatRouterHandler +from dandelion.mqtt.service.rsu.rsu_info import RSUInfoRouterHandler +from dandelion.mqtt.service.rsu.rsu_running_info import RSURunningInfoRouterHandler + +LOG: LoggerAdapter = log.getLogger(__name__) +CONF: cfg = conf.CONF + +topic_router: Dict[str, RouterHandler] = { + "V2X/RSU/INFO/UP": RSUInfoRouterHandler(), + "V2X/RSU/HB/UP": RSUHeartbeatRouterHandler(), + "V2X/RSU/BaseINFO/UP": RSUBaseINFORouterHandler(), + "V2X/RSU/RunningInfo/UP": RSURunningInfoRouterHandler(), + "V2X/RSU/+/MAP/UP": MapRouterHandler(), + "V2X/DEVICE/+/RSI/UP": RSIRouterHandler(), + "V2X/DEVICE/+/RSM_STRUCTURE": RSMRouterHandler(), +} +MQTT_CLIENT: mqtt.Client = None +GET_MQTT_CLIENT: Callable[[], mqtt.Client] + + +def _get_mqtt() -> mqtt.Client: + global MQTT_CLIENT + if MQTT_CLIENT is None: + raise SystemError("MQTT Client is none") + return MQTT_CLIENT + + +def _on_connect(client: mqtt.Client, userdata: Any, flags: Any, rc: int) -> None: + if rc != 0: + raise SystemError("MQTT Connection failed") + LOG.info("MQTT Connection succeeded") + + global MQTT_CLIENT + MQTT_CLIENT = client + + global GET_MQTT_CLIENT + GET_MQTT_CLIENT = _get_mqtt + + for route in topic_router: + client.message_callback_add(route, topic_router[route].request) + client.subscribe(topic=route, qos=0) + + +def _on_message(client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage) -> None: + LOG.info(msg.payload.decode("utf-8")) + + +def _on_disconnect(client: mqtt.Client, userdata: Any, flags: Any, rc: int) -> None: + LOG.error(f"MQTT Connection disconnected, rc: {rc}") + + +def connect() -> None: + mqtt_conf = CONF.mqtt + + _client = mqtt.Client(client_id=uuid.uuid4().hex) + _client.username_pw_set(mqtt_conf.username, mqtt_conf.password) + _client.on_connect = _on_connect + _client.on_message = _on_message + _client.on_disconnect = _on_disconnect + _client.connect(mqtt_conf.host, mqtt_conf.port, 60) + _client.loop_start() diff --git a/dandelion/mqtt/service/__init__.py b/dandelion/mqtt/service/__init__.py new file mode 100644 index 0000000..36376c7 --- /dev/null +++ b/dandelion/mqtt/service/__init__.py @@ -0,0 +1,41 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class RouterHandler(object): + def request( + self, _client: mqtt.MQTT_CLIENT, _user_data: Dict[str, Any], _msg: mqtt.MQTTMessage + ) -> None: + try: + topic_ = _msg.topic + msg_ = _msg.payload.decode("utf-8") + LOG.info(f"{topic_} => {msg_}") + data_ = json.loads(msg_) + self.handler(_client, topic_, data_) + except Exception as ex: + LOG.error(ex) + + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + """""" diff --git a/dandelion/mqtt/service/algorithm/__init__.py b/dandelion/mqtt/service/algorithm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/mqtt/service/algorithm/rsi.py b/dandelion/mqtt/service/algorithm/rsi.py new file mode 100644 index 0000000..f87720f --- /dev/null +++ b/dandelion/mqtt/service/algorithm/rsi.py @@ -0,0 +1,57 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, schemas +from dandelion.db import session +from dandelion.mqtt.service import RouterHandler + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class RSIRouterHandler(RouterHandler): + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + db: Session = session.DB_SESSION_LOCAL() + + rsi = data.get("rsi") + if not rsi: + LOG.warn(f"{topic} => rsi is None") + return None + rsu = crud.rsu.get_first(db) + rsi_event_in = schemas.RSIEventCreate( + alertID=rsi.get("alertID"), + duration=rsi.get("duration"), + eventStatus=rsi.get("eventStatus"), + timeStamp=rsi.get("timeStamp"), + eventClass=rsi.get("eventClass"), + eventType=rsi.get("eventType"), + eventSource=rsi.get("eventSource"), + eventConfidence=rsi.get("eventConfidence"), + eventPosition=rsi.get("eventPosition"), + eventRadius=rsi.get("eventRadius"), + eventDescription=rsi.get("eventDescription"), + eventPriority=rsi.get("eventPriority"), + referencePaths=rsi.get("referencePaths"), + areaCode=rsi.get("areaCode"), + ) + crud.rsi_event.create_rsi_event(db, obj_in=rsi_event_in, rsu=rsu) + LOG.info(f"{topic} => RSIEvent [alert_id: {rsi_event_in.alert_id}] created") diff --git a/dandelion/mqtt/service/algorithm/rsm.py b/dandelion/mqtt/service/algorithm/rsm.py new file mode 100644 index 0000000..c940356 --- /dev/null +++ b/dandelion/mqtt/service/algorithm/rsm.py @@ -0,0 +1,58 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict, List + +import paho.mqtt.client as mqtt +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models, schemas +from dandelion.db import session +from dandelion.mqtt.service import RouterHandler +from dandelion.util import Optional as Optional_util + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class RSMRouterHandler(RouterHandler): + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + db: Session = session.DB_SESSION_LOCAL() + + rsms = Optional_util.none(data.get("content")).map(lambda v: v.get("rsms")).get() + for rsm_ in rsms: + rsm = schemas.RSMCreate(refPos=rsm_.get("refPos")) + + ps = rsm_.get("participants") + if not ps: + LOG.info(f"{topic} => RSM has no participants") + return None + participants: List[models.Participants] = [] + for p_ in ps: + p = models.Participants() + p.ptc_id = p_.get("ptcId") + p.ptc_type = p_.get("ptcType") + p.source = p_.get("source") + p.sec_mark = p_.get("secMark") + p.pos = p_.get("pos") + p.accuracy = p_.get("accuracy") + p.speed = p_.get("speed") + p.heading = p_.get("heading") + p.size = p_.get("size", {}) + participants.append(p) + crud.rsm.create_rsm(db, obj_in=rsm, participants=participants) + LOG.info(f"{topic} => RSM created") diff --git a/dandelion/mqtt/service/log/__init__.py b/dandelion/mqtt/service/log/__init__.py new file mode 100644 index 0000000..982b0a1 --- /dev/null +++ b/dandelion/mqtt/service/log/__init__.py @@ -0,0 +1,33 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict, Optional + +from oslo_log import log + +from dandelion.mqtt import send_msg +from dandelion.mqtt.topic.rsu_log import v2x_rsu_log_conf_down, v2x_rsu_log_conf_down_all + +LOG: LoggerAdapter = log.getLogger(__name__) + + +def log_down(data: Dict[str, Any], rsu_esn: Optional[str] = None) -> None: + LOG.info(f"log_down: rsu_esn={rsu_esn}, data={data}") + topic = v2x_rsu_log_conf_down_all() + if rsu_esn is not None: + topic = v2x_rsu_log_conf_down(rsu_esn) + send_msg(topic, data) diff --git a/dandelion/mqtt/service/map/__init__.py b/dandelion/mqtt/service/map/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/mqtt/service/map/map_down.py b/dandelion/mqtt/service/map/map_down.py new file mode 100644 index 0000000..3554122 --- /dev/null +++ b/dandelion/mqtt/service/map/map_down.py @@ -0,0 +1,33 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import logging +from typing import Any, Dict, Optional + +from dandelion.mqtt import send_msg +from dandelion.mqtt.topic.map import v2x_rsu_map_down, v2x_rsu_map_down_all + +logger = logging.getLogger(__name__) + + +def map_down( + map_slice: str, map_: Dict[str, Any], e_tag: str, rsu_esn: Optional[str] = None +) -> None: + data = dict(mapSlice=map_slice, map=map_, eTag=e_tag, ack=False) + topic = v2x_rsu_map_down_all() + if rsu_esn is not None: + topic = v2x_rsu_map_down(rsu_esn) + send_msg(topic, data) diff --git a/dandelion/mqtt/service/map/map_up.py b/dandelion/mqtt/service/map/map_up.py new file mode 100644 index 0000000..8738150 --- /dev/null +++ b/dandelion/mqtt/service/map/map_up.py @@ -0,0 +1,46 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, models +from dandelion.db import session +from dandelion.mqtt.service import RouterHandler + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class MapRouterHandler(RouterHandler): + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + db: Session = session.DB_SESSION_LOCAL() + + rsu = crud.rsu.get_first(db) + map_ = models.Map( + name=data.get("mapSlice"), + address=rsu.address, + area_code=rsu.area_code, + desc=data.get("eTag"), + data=data.get("map"), + lng=data.get("map", {}).get("refPos", {}).get("lon", 0), + lat=data.get("map", {}).get("refPos", {}).get("lat", 0), + ) + crud.map.create(db, obj_in=map_) + LOG.info(f"{topic} => Map [name: {map_.name}] created") diff --git a/dandelion/mqtt/service/mng/__init__.py b/dandelion/mqtt/service/mng/__init__.py new file mode 100644 index 0000000..3764c95 --- /dev/null +++ b/dandelion/mqtt/service/mng/__init__.py @@ -0,0 +1,30 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict + +from oslo_log import log + +from dandelion.mqtt import send_msg +from dandelion.mqtt.topic.mng import v2x_rsu_mng_down + +LOG: LoggerAdapter = log.getLogger(__name__) + + +def mng_down(rsu_esn: str, data: Dict[str, Any]) -> None: + LOG.info(f"mng_down: rsu_esn={rsu_esn}, data={data}") + send_msg(v2x_rsu_mng_down(rsu_esn), data) diff --git a/dandelion/mqtt/service/query/__init__.py b/dandelion/mqtt/service/query/__init__.py new file mode 100644 index 0000000..c584cd0 --- /dev/null +++ b/dandelion/mqtt/service/query/__init__.py @@ -0,0 +1,33 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import time + +from dandelion.mqtt import send_msg +from dandelion.mqtt.topic.query import info_query + + +def down_query(rsu_id: int, rsu_esn: str, version: str, info_id: str, interval: int) -> None: + data = dict( + rsuId=rsu_id, + rsuEsn=rsu_esn, + timestamp=int(time.time()), + protocolVersion=version, + infoId=info_id, + interval=interval, + ack=False, + ) + send_msg(info_query(), data) diff --git a/dandelion/mqtt/service/rsu/__init__.py b/dandelion/mqtt/service/rsu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/mqtt/service/rsu/rsu_base_info.py b/dandelion/mqtt/service/rsu/rsu_base_info.py new file mode 100644 index 0000000..ebb611c --- /dev/null +++ b/dandelion/mqtt/service/rsu/rsu_base_info.py @@ -0,0 +1,30 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log + +from dandelion.mqtt.service import RouterHandler + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class RSUBaseINFORouterHandler(RouterHandler): + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + LOG.warn(f"{topic} => Not implemented yet") diff --git a/dandelion/mqtt/service/rsu/rsu_config.py b/dandelion/mqtt/service/rsu/rsu_config.py new file mode 100644 index 0000000..7ed7a3c --- /dev/null +++ b/dandelion/mqtt/service/rsu/rsu_config.py @@ -0,0 +1,33 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict, Optional + +from oslo_log import log + +from dandelion.mqtt import send_msg +from dandelion.mqtt.topic.rsu_config import v2x_rsu_config_down, v2x_rsu_config_down_all + +LOG: LoggerAdapter = log.getLogger(__name__) + + +def config_down(data: Dict[str, Any], rsu_esn: Optional[str] = None) -> None: + LOG.info(f"config_down: rsu_esn={rsu_esn}, data={data}") + topic = v2x_rsu_config_down_all() + if rsu_esn is not None: + topic = v2x_rsu_config_down(rsu_esn) + send_msg(topic, data) diff --git a/dandelion/mqtt/service/rsu/rsu_heartbeat.py b/dandelion/mqtt/service/rsu/rsu_heartbeat.py new file mode 100644 index 0000000..b70fd17 --- /dev/null +++ b/dandelion/mqtt/service/rsu/rsu_heartbeat.py @@ -0,0 +1,49 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, schemas +from dandelion.api.deps import get_redis_conn +from dandelion.db import session +from dandelion.mqtt.service import RouterHandler + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class RSUHeartbeatRouterHandler(RouterHandler): + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + db: Session = session.DB_SESSION_LOCAL() + redis_conn = get_redis_conn() + + rsu_esn = data.get("rsuEsn") + if not rsu_esn: + LOG.warn(f"{topic} => rsu_esn is None") + return None + rsu = crud.rsu.get_by_rsu_esn(db, rsu_esn=rsu_esn) + if not rsu: + LOG.info(f"{topic} => RSU [rsu_esn: {rsu_esn}] not found") + return None + crud.rsu.update_online_status( + db, db_obj=rsu, obj_in=schemas.RSUUpdateWithStatus(onlineStatus=True) + ) + redis_conn.set(f"RSU_ONLINE_{rsu_esn}", 1, ex=15) + LOG.info(f"{topic} => RSU [rsu_esn: {rsu_esn}] onlineStatus updated") diff --git a/dandelion/mqtt/service/rsu/rsu_info.py b/dandelion/mqtt/service/rsu/rsu_info.py new file mode 100644 index 0000000..69c0eb5 --- /dev/null +++ b/dandelion/mqtt/service/rsu/rsu_info.py @@ -0,0 +1,62 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, schemas +from dandelion.db import session +from dandelion.mqtt.service import RouterHandler + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class RSUInfoRouterHandler(RouterHandler): + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + db: Session = session.DB_SESSION_LOCAL() + + rsu_esn = data.get("rsuEsn") + if not rsu_esn: + LOG.warn(f"{topic} => rsu_esn is None") + return None + + rsu = crud.rsu.get_by_rsu_esn(db, rsu_esn=rsu_esn) + if not rsu: + LOG.info(f"{topic} => RSU not found: {rsu_esn}") + total, _ = crud.rsu_tmp.get_multi_with_total(db, rsu_esn=rsu_esn) + if total > 0: + LOG.info( + f"{topic} => RSU Tmp [rsu_esn: {rsu_esn}] total: {total}, " + "ignore to create RSU Tmp" + ) + return None + rsu_tmp = schemas.RSUTMPCreate(**data) + crud.rsu_tmp.create(db, obj_in=rsu_tmp) + LOG.info(f"{topic} => RSU Tmp [rsu_esn: {rsu_esn}] created") + else: + rsu_in = schemas.RSUUpdateWithVersion( + rsuId=data.get("rsuId"), + rsuName=data.get("rsuName"), + version=data.get("version"), + location=data.get("location"), + config=data.get("config"), + ) + crud.rsu.update_with_version(db, db_obj=rsu, obj_in=rsu_in) + LOG.info(f"{topic} => RSU [rsu_esn: {rsu_esn}] updated") diff --git a/dandelion/mqtt/service/rsu/rsu_running_info.py b/dandelion/mqtt/service/rsu/rsu_running_info.py new file mode 100644 index 0000000..e3b9330 --- /dev/null +++ b/dandelion/mqtt/service/rsu/rsu_running_info.py @@ -0,0 +1,30 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter +from typing import Any, Dict + +import paho.mqtt.client as mqtt +from oslo_log import log + +from dandelion.mqtt.service import RouterHandler + +LOG: LoggerAdapter = log.getLogger(__name__) + + +class RSURunningInfoRouterHandler(RouterHandler): + def handler(self, client: mqtt.MQTT_CLIENT, topic: str, data: Dict[str, Any]) -> None: + LOG.warn(f"{topic} => Not implemented yet") diff --git a/dandelion/mqtt/topic/__init__.py b/dandelion/mqtt/topic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/mqtt/topic/map.py b/dandelion/mqtt/topic/map.py new file mode 100644 index 0000000..252a6b0 --- /dev/null +++ b/dandelion/mqtt/topic/map.py @@ -0,0 +1,31 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def v2x_rsu_map_up_ack(rsu_id): + return f"V2X/RSU/{rsu_id}/MAP/UP/ACK" + + +def v2x_rsu_map_down(rsu_id): + return f"V2X/RSU/{rsu_id}/MAP/DOWN" + + +def v2x_rsu_map_down_all(): + return "V2X/RSU/MAP/DOWN" + + +def v2x_rsu_map_down_ack(): + return "V2X/RSU/MAP/DOWN/ACK" diff --git a/dandelion/mqtt/topic/mng.py b/dandelion/mqtt/topic/mng.py new file mode 100644 index 0000000..a14ab85 --- /dev/null +++ b/dandelion/mqtt/topic/mng.py @@ -0,0 +1,23 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def v2x_rsu_mng_down(rsu_esn): + return f"V2X/RSU/{rsu_esn}/MNG/DOWN" + + +def v2x_rsu_mng_down_ack(rsu_esn): + return f"V2X/RSU/{rsu_esn}/MNG/DOWN/ACK" diff --git a/dandelion/mqtt/topic/query.py b/dandelion/mqtt/topic/query.py new file mode 100644 index 0000000..d2f14a3 --- /dev/null +++ b/dandelion/mqtt/topic/query.py @@ -0,0 +1,23 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def info_query(): + return "V2X/RSU/INFOQuery" + + +def info_query_response(): + return "V2X/RSU/INFOQuery/Response" diff --git a/dandelion/mqtt/topic/rsu_config.py b/dandelion/mqtt/topic/rsu_config.py new file mode 100644 index 0000000..db50ae3 --- /dev/null +++ b/dandelion/mqtt/topic/rsu_config.py @@ -0,0 +1,35 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def v2x_rsu_config_up(): + return "V2X/RSU/CONFIG/UP" + + +def v2x_rsu_config_up_ack(rsu_id): + return f"V2X/RSU/{rsu_id}/CONFIG/UP/ACK" + + +def v2x_rsu_config_down(rsu_id): + return f"V2X/RSU/{rsu_id}/CONFIG/DOWN" + + +def v2x_rsu_config_down_all(): + return "V2X/RSU/CONFIG/DOWN" + + +def v2x_rsu_config_down_ack(): + return "V2X/RSU/CONFIG/DOWN/ACK" diff --git a/dandelion/mqtt/topic/rsu_heartbeat.py b/dandelion/mqtt/topic/rsu_heartbeat.py new file mode 100644 index 0000000..ef1f604 --- /dev/null +++ b/dandelion/mqtt/topic/rsu_heartbeat.py @@ -0,0 +1,23 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def v2x_rsu_hb_up(): + return "V2X/RSU/HB/UP" + + +def v2x_rsu_hb_up_ack(rsu_id): + return f"V2X/RSU/{rsu_id}/HB/UP/ACK" diff --git a/dandelion/mqtt/topic/rsu_info.py b/dandelion/mqtt/topic/rsu_info.py new file mode 100644 index 0000000..fa2edea --- /dev/null +++ b/dandelion/mqtt/topic/rsu_info.py @@ -0,0 +1,23 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def v2x_rsu_info_up(): + return "V2X/RSU/INFO/UP" + + +def v2x_rsu_info_up_ack(rsu_id): + return f"V2X/RSU/{rsu_id}/INFO/UP/ACK" diff --git a/dandelion/mqtt/topic/rsu_log.py b/dandelion/mqtt/topic/rsu_log.py new file mode 100644 index 0000000..cf64c5f --- /dev/null +++ b/dandelion/mqtt/topic/rsu_log.py @@ -0,0 +1,27 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + + +def v2x_rsu_log_conf_down_all(): + return "V2X/RSU/LOG/UP" + + +def v2x_rsu_log_conf_down(rsu_esn): + return f"V2X/RSU/{rsu_esn}/LOG/UP" + + +def v2x_rsu_log_conf_down_ack(rsu_esn): + return f"V2X/RSU/{rsu_esn}/LOG/UP/ACK" diff --git a/dandelion/periodic_tasks.py b/dandelion/periodic_tasks.py new file mode 100644 index 0000000..220f2d6 --- /dev/null +++ b/dandelion/periodic_tasks.py @@ -0,0 +1,44 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from logging import LoggerAdapter + +import redis +from oslo_log import log +from sqlalchemy.orm import Session + +from dandelion import crud, schemas +from dandelion.db import redis_pool, session + +LOG: LoggerAdapter = log.getLogger(__name__) + + +def update_rsu_online_status() -> None: + LOG.info("Updating RSU online status...") + db: Session = session.DB_SESSION_LOCAL() + redis_conn: redis.Redis = redis_pool.REDIS_CONN + + _, online_rsus = crud.rsu.get_multi_with_total(db, online_status=True) + LOG.debug(f"Found {len(online_rsus)} online RSUs") + for rsu in online_rsus: + if redis_conn.get(f"RSU_ONLINE_{rsu.rsu_esn}"): + continue + try: + crud.rsu.update_online_status( + db, db_obj=rsu, obj_in=schemas.RSUUpdateWithStatus(onlineStatus=False) + ) + except Exception as ex: + LOG.warn(f"Failed to update RSU [rsu_esn: {rsu.rsu_esn}] online status: {ex}") diff --git a/dandelion/py.typed b/dandelion/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/schemas/__init__.py b/dandelion/schemas/__init__.py new file mode 100644 index 0000000..eca72eb --- /dev/null +++ b/dandelion/schemas/__init__.py @@ -0,0 +1,61 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# flake8: noqa: F401 + +from __future__ import annotations + +from .area import Area, AreaCreate, AreaUpdate +from .camera import Camera, CameraCreate, Cameras, CameraUpdate +from .city import City, CityCreate, CityUpdate +from .cloud_home import OnlineRate, RouteInfo, RouteInfoCreate +from .country import Country, CountryCreate, CountryUpdate +from .map import Map, MapCreate, Maps, MapUpdate +from .map_rsu import MapRSU, MapRSUCreate, MapRSUs, MapRSUUpdate +from .message import ErrorMessage, Message +from .mng import MNG, MNGCopy, MNGCreate, MNGs, MNGUpdate +from .province import Province, ProvinceCreate, ProvinceUpdate +from .radar import Radar, RadarCreate, Radars, RadarUpdate +from .rsi_event import RSIEvent, RSIEventCreate, RSIEvents, RSIEventUpdate +from .rsm import RSM, RSMCreate, RSMs, RSMUpdate +from .rsm_participant import ( + RSMParticipant, + RSMParticipantCreate, + RSMParticipants, + RSMParticipantUpdate, +) +from .rsu import ( + RSU, + RSUCreate, + RSUDetail, + RSULocation, + RSUs, + RSUUpdate, + RSUUpdateWithStatus, + RSUUpdateWithVersion, +) +from .rsu_config import RSUConfig, RSUConfigCreate, RSUConfigs, RSUConfigUpdate, RSUConfigWithRSUs +from .rsu_config_rsu import RSUConfigRSU, RSUConfigRSUCreate, RSUConfigRSUs, RSUConfigRSUUpdate +from .rsu_log import RSULog, RSULogCreate, RSULogs, RSULogUpdate +from .rsu_model import RSUModel, RSUModelCreate, RSUModels, RSUModelUpdate +from .rsu_query import RSUQueries, RSUQuery, RSUQueryCreate, RSUQueryDetail, RSUQueryUpdate +from .rsu_query_result import ( + RSUQueryResult, + RSUQueryResultCreate, + RSUQueryResults, + RSUQueryResultUpdate, +) +from .rsu_tmp import RSUTMP, RSUTMPCreate, RSUTMPs, RSUTMPUpdate +from .token import AccessToken, Token, TokenPayload +from .user import User, UserCreate, UserUpdate diff --git a/dandelion/schemas/area.py b/dandelion/schemas/area.py new file mode 100644 index 0000000..7437af4 --- /dev/null +++ b/dandelion/schemas/area.py @@ -0,0 +1,43 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +# Shared properties +class AreaBase(BaseModel): + code: str = Field(..., alias="code", description="Area code") + name: str = Field(..., alias="name", description="Area name") + + +# Properties to receive via API on creation +class AreaCreate(AreaBase): + """""" + + +# Properties to receive via API on update +class AreaUpdate(AreaBase): + """""" + + +class AreaInDBBase(AreaBase): + class Config: + orm_mode = True + + +# Additional properties to return via API +class Area(AreaInDBBase): + """""" diff --git a/dandelion/schemas/camera.py b/dandelion/schemas/camera.py new file mode 100644 index 0000000..24d0ab3 --- /dev/null +++ b/dandelion/schemas/camera.py @@ -0,0 +1,86 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class CameraBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class CameraCreate(BaseModel): + sn: str = Field(..., alias="sn", description="SN") + name: str = Field(..., alias="name", description="Name") + stream_url: str = Field(..., alias="streamUrl", description="Stream URL") + lng: float = Field(..., alias="lng", description="Lng") + lat: float = Field(..., alias="lat", description="Lat") + elevation: float = Field(..., alias="elevation", description="Elevation") + towards: float = Field(..., alias="towards", description="Towards") + rsu_id: int = Field(..., alias="rsuId", description="RSU ID") + desc: Optional[str] = Field(None, alias="desc", description="Description") + + +# Properties to receive via API on update +class CameraUpdate(CameraBase): + sn: str = Field(..., alias="sn", description="SN") + name: str = Field(..., alias="name", description="Name") + stream_url: str = Field(..., alias="streamUrl", description="Stream URL") + lng: float = Field(..., alias="lng", description="Lng") + lat: float = Field(..., alias="lat", description="Lat") + elevation: float = Field(..., alias="elevation", description="Elevation") + towards: float = Field(..., alias="towards", description="Towards") + rsu_id: int = Field(..., alias="rsuId", description="RSU ID") + desc: Optional[str] = Field(None, alias="desc", description="Description") + + +class CameraInDBBase(CameraBase): + id: int = Field(..., alias="id", description="Camera ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class Camera(CameraInDBBase): + sn: str = Field(..., alias="sn", description="SN") + name: str = Field(..., alias="name", description="Name") + stream_url: str = Field(..., alias="streamUrl", description="Stream URL") + lng: float = Field(..., alias="lng", description="Lng") + lat: float = Field(..., alias="lat", description="Lat") + elevation: float = Field(..., alias="elevation", description="Elevation") + towards: float = Field(..., alias="towards", description="Towards") + rsu_id: int = Field(..., alias="rsuId", description="RSU ID") + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + country_code: str = Field(..., alias="countryCode", description="Country Code") + country_name: str = Field(..., alias="countryName", description="Country Name") + province_code: str = Field(..., alias="provinceCode", description="Province Code") + province_name: str = Field(..., alias="provinceName", description="Province Name") + city_code: str = Field(..., alias="cityCode", description="City Code") + city_name: str = Field(..., alias="cityName", description="City Name") + area_code: str = Field(..., alias="areaCode", description="Area Code") + area_name: str = Field(..., alias="areaName", description="Area Name") + desc: str = Field(..., alias="desc", description="Description") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + + +class Cameras(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[Camera] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/city.py b/dandelion/schemas/city.py new file mode 100644 index 0000000..853ee93 --- /dev/null +++ b/dandelion/schemas/city.py @@ -0,0 +1,43 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +# Shared properties +class CityBase(BaseModel): + code: str = Field(..., alias="code", description="City code") + name: str = Field(..., alias="name", description="City name") + + +# Properties to receive via API on creation +class CityCreate(CityBase): + """""" + + +# Properties to receive via API on update +class CityUpdate(CityBase): + """""" + + +class CityInDBBase(CityBase): + class Config: + orm_mode = True + + +# Additional properties to return via API +class City(CityInDBBase): + """""" diff --git a/dandelion/schemas/cloud_home.py b/dandelion/schemas/cloud_home.py new file mode 100644 index 0000000..c438f06 --- /dev/null +++ b/dandelion/schemas/cloud_home.py @@ -0,0 +1,64 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, Field + + +class RSUOnlineRateBase(BaseModel): + online: int = Field(..., alias="online", description="Online") + offline: int = Field(..., alias="offline", description="Offline") + not_register: int = Field(..., alias="notRegister", description="Not Register") + + +class CameraOnlineRateBase(BaseModel): + online: int = Field(..., alias="online", description="Online") + offline: int = Field(..., alias="offline", description="Offline") + not_register: int = Field(..., alias="notRegister", description="Not Register") + + +class RadarOnlineRateBase(BaseModel): + online: int = Field(..., alias="online", description="Online") + offline: int = Field(..., alias="offline", description="Offline") + not_register: int = Field(..., alias="notRegister", description="Not Register") + + +class OnlineRateBase(BaseModel): + rsu: RSUOnlineRateBase = Field(..., alias="rsu", description="RSU Online Rate") + camera: CameraOnlineRateBase = Field(..., alias="camera", description="Camera Online Rate") + radar: RadarOnlineRateBase = Field(..., alias="radar", description="Radar Online Rate") + + +class OnlineRate(BaseModel): + data: OnlineRateBase = Field(..., alias="data", description="Online Rate") + + +class RouteInfo(BaseModel): + vehicle_total: int = Field(..., alias="vehicleTotal", description="Vehicle Total") + average_speed: int = Field(..., alias="averageSpeed", description="Average Speed") + pedestrian_total: int = Field(..., alias="pedestrianTotal", description="Pedestrian Total") + congestion: str = Field(..., alias="congestion", description="Congestion") + + +class RouteInfoCreate(BaseModel): + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + vehicle_total: Optional[int] = Field(0, alias="vehicleTotal", description="Vehicle Total") + average_speed: Optional[int] = Field(0, alias="averageSpeed", description="Average Speed") + pedestrian_total: Optional[int] = Field( + 0, alias="pedestrianTotal", description="Pedestrian Total" + ) + congestion: Optional[str] = Field("Unknown", alias="congestion", description="Congestion") diff --git a/dandelion/schemas/country.py b/dandelion/schemas/country.py new file mode 100644 index 0000000..6c730c0 --- /dev/null +++ b/dandelion/schemas/country.py @@ -0,0 +1,45 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class CountryBase(BaseModel): + code: str = Field(..., alias="code", description="Country code") + name: str = Field(..., alias="name", description="Country name") + + +# Properties to receive via API on creation +class CountryCreate(CountryBase): + """""" + + +# Properties to receive via API on update +class CountryUpdate(CountryBase): + """""" + + +class CountryInDBBase(CountryBase): + class Config: + orm_mode = True + + +# Additional properties to return via API +class Country(CountryInDBBase): + children: Optional[List] = None diff --git a/dandelion/schemas/map.py b/dandelion/schemas/map.py new file mode 100644 index 0000000..e085675 --- /dev/null +++ b/dandelion/schemas/map.py @@ -0,0 +1,74 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class MapBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class MapCreate(BaseModel): + name: str = Field(..., alias="name", description="Map Name") + area_code: str = Field(..., alias="areaCode", description="Area Code") + address: str = Field(..., alias="address", description="Address") + desc: Optional[str] = Field("", alias="desc", description="Description") + data: Dict[str, Any] = Field(..., alias="data", description="Data") + + +# Properties to receive via API on update +class MapUpdate(MapBase): + name: str = Field(..., alias="name", description="Name") + area_code: str = Field(..., alias="areaCode", description="Area Code") + address: str = Field(..., alias="address", description="Address") + desc: Optional[str] = Field(None, alias="desc", description="Description") + data: Optional[Dict[str, Any]] = Field(None, alias="data", description="Data") + + +class MapInDBBase(MapBase): + id: int = Field(..., alias="id", description="Map ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class Map(MapInDBBase): + name: str = Field(..., alias="name", description="Map Name") + address: str = Field(..., alias="address", description="Address") + desc: str = Field(..., alias="desc", description="Description") + amount: int = Field(..., alias="amount", description="Count of RSUs") + lat: float = Field(..., alias="lat", description="Latitude") + lng: float = Field(..., alias="lng", description="Longitude") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + country_code: str = Field(..., alias="countryCode", description="Country Code") + country_name: str = Field(..., alias="countryName", description="Country Name") + province_code: str = Field(..., alias="provinceCode", description="Province Code") + province_name: str = Field(..., alias="provinceName", description="Province Name") + city_code: str = Field(..., alias="cityCode", description="City Code") + city_name: str = Field(..., alias="cityName", description="City Name") + area_code: str = Field(..., alias="areaCode", description="Area Code") + area_name: str = Field(..., alias="areaName", description="Area Name") + + +class Maps(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[Map] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/map_rsu.py b/dandelion/schemas/map_rsu.py new file mode 100644 index 0000000..1ff1b6e --- /dev/null +++ b/dandelion/schemas/map_rsu.py @@ -0,0 +1,69 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import List + +from pydantic import BaseModel, Field + + +class RSUsInMapRSU(BaseModel): + id: int = Field(..., alias="id", description="ID") + rsu_id: int = Field(..., alias="rsuId", description="RSU ID") + status: int = Field(..., alias="status", description="Status") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + + +# Shared properties +class MapRSUBase(BaseModel): + map_id: int = Field(..., alias="mapId", description="Map ID") + rsus: List[RSUsInMapRSU] = Field(..., alias="rsus", description="RSUs") + + +# Properties to receive via API on creation +class MapRSUCreate(BaseModel): + rsus: List[str] = Field(..., alias="rsus", description="RSU ESN") + + +# Properties to receive via API on update +class MapRSUUpdate(MapRSUBase): + """""" + + +class MapRSUInDBBase(BaseModel): + id: int = Field(..., alias="id", description="Map RSU ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class MapRSU(BaseModel): + data: MapRSUBase = Field(..., alias="data", description="Data") + + +class MapRSUsBase(MapRSUInDBBase): + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + rsu_sn: str = Field(..., alias="rsuSn", description="RSU SN") + online_status: int = Field(..., alias="onlineStatus", description="Online Status") + rsu_status: int = Field(..., alias="rsuStatus", description="RSU Status") + delivery_status: int = Field(..., alias="deliveryStatus", description="Delivery Status") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + + +class MapRSUs(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[MapRSUsBase] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/message.py b/dandelion/schemas/message.py new file mode 100644 index 0000000..4fafa04 --- /dev/null +++ b/dandelion/schemas/message.py @@ -0,0 +1,29 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class MessageBase(BaseModel): + detail: str = Field(..., alias="detail", description="Message detail") + + +class Message(MessageBase): + """""" + + +class ErrorMessage(MessageBase): + """""" diff --git a/dandelion/schemas/mng.py b/dandelion/schemas/mng.py new file mode 100644 index 0000000..67c1a79 --- /dev/null +++ b/dandelion/schemas/mng.py @@ -0,0 +1,88 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import List + +from pydantic import BaseModel, Field + + +class LogLevel(str, Enum): + DEBUG = "DEBUG" + INFO = "INFO" + WARN = "WARN" + ERROR = "ERROR" + NOLog = "NOLog" + + +class Reboot(str, Enum): + not_reboot = "not_reboot" + reboot = "reboot" + + +class AddressChg(BaseModel): + cssUrl: str = Field(..., alias="cssUrl", description="CSS URL") + time: int = Field(..., alias="time", description="Time") + + +# Shared properties +class MNGBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class MNGCreate(BaseModel): + """""" + + +# Properties to receive via API on update +class MNGUpdate(MNGBase): + heartbeat_rate: int = Field(..., alias="hbRate", description="Heartbeat Rate") + running_info_rate: int = Field(..., alias="runningInfoRate", description="Running Info Rate") + address_change: AddressChg = Field(..., alias="addressChg", description="Address Change") + log_level: LogLevel = Field(..., alias="logLevel", description="Log Level") + reboot: Reboot = Field(..., alias="reboot", description="Reboot") + extend_config: str = Field(..., alias="extendConfig", description="Extend Config") + + +class MNGInDBBase(MNGBase): + id: int = Field(..., alias="id", description="MNG ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class MNG(MNGInDBBase): + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + heartbeat_rate: int = Field(..., alias="hbRate", description="Heartbeat Rate") + running_info_rate: int = Field(..., alias="runningInfoRate", description="Running Info Rate") + address_change: AddressChg = Field(..., alias="addressChg", description="Address Change") + log_level: LogLevel = Field(..., alias="logLevel", description="Log Level") + reboot: Reboot = Field(..., alias="reboot", description="Reboot") + extend_config: str = Field(..., alias="extendConfig", description="Extend Config") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + + +class MNGs(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[MNG] = Field(..., alias="data", description="Data") + + +class MNGCopy(BaseModel): + rsus: List[int] = Field(..., alias="rsus", description="RSUs") diff --git a/dandelion/schemas/province.py b/dandelion/schemas/province.py new file mode 100644 index 0000000..4c658e0 --- /dev/null +++ b/dandelion/schemas/province.py @@ -0,0 +1,43 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +# Shared properties +class ProvinceBase(BaseModel): + code: str = Field(..., alias="code", description="Province code") + name: str = Field(..., alias="name", description="Province name") + + +# Properties to receive via API on creation +class ProvinceCreate(ProvinceBase): + """""" + + +# Properties to receive via API on update +class ProvinceUpdate(ProvinceBase): + """""" + + +class ProvinceInDBBase(ProvinceBase): + class Config: + orm_mode = True + + +# Additional properties to return via API +class Province(ProvinceInDBBase): + """""" diff --git a/dandelion/schemas/radar.py b/dandelion/schemas/radar.py new file mode 100644 index 0000000..9a0b094 --- /dev/null +++ b/dandelion/schemas/radar.py @@ -0,0 +1,86 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class RadarBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RadarCreate(BaseModel): + sn: str = Field(..., alias="sn", description="Radar SN") + name: str = Field(..., alias="name", description="Radar Name") + radar_ip: Optional[str] = Field(None, alias="radarIp", description="Radar IP") + lng: str = Field(..., alias="lng", description="Longitude") + lat: str = Field(..., alias="lat", description="Latitude") + elevation: str = Field(..., alias="elevation", description="Elevation") + towards: str = Field(..., alias="towards", description="Towards") + rsu_id: Optional[int] = Field(None, alias="rsuId", description="RSU ID") + desc: Optional[str] = Field("", alias="desc", description="Description") + + +# Properties to receive via API on update +class RadarUpdate(RadarBase): + sn: str = Field(..., alias="sn", description="Radar SN") + name: str = Field(..., alias="name", description="Radar Name") + radar_ip: Optional[str] = Field(None, alias="radarIp", description="Radar IP") + lng: str = Field(..., alias="lng", description="Longitude") + lat: str = Field(..., alias="lat", description="Latitude") + elevation: str = Field(..., alias="elevation", description="Elevation") + towards: str = Field(..., alias="towards", description="Towards") + rsu_id: Optional[int] = Field(None, alias="rsuId", description="RSU ID") + desc: Optional[str] = Field("", alias="desc", description="Description") + + +class RadarInDBBase(RadarBase): + id: int = Field(..., alias="id", description="Radar ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class Radar(RadarInDBBase): + sn: str = Field(..., alias="sn", description="Radar SN") + name: str = Field(..., alias="name", description="Radar Name") + radar_ip: str = Field(..., alias="radarIP", description="Radar IP") + lng: str = Field(..., alias="lng", description="Longitude") + lat: str = Field(..., alias="lat", description="Latitude") + elevation: str = Field(..., alias="elevation", description="Elevation") + towards: str = Field(..., alias="towards", description="Towards") + rsu_id: int = Field(..., alias="rsuId", description="RSU ID") + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + country_code: str = Field(..., alias="countryCode", description="Country Code") + country_name: str = Field(..., alias="countryName", description="Country Name") + province_code: str = Field(..., alias="provinceCode", description="Province Code") + province_name: str = Field(..., alias="provinceName", description="Province Name") + city_code: str = Field(..., alias="cityCode", description="City Code") + city_name: str = Field(..., alias="cityName", description="City Name") + area_code: str = Field(..., alias="areaCode", description="Area Code") + area_name: str = Field(..., alias="areaName", description="Area Name") + desc: str = Field(..., alias="desc", description="Description") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + + +class Radars(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[Radar] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsi_event.py b/dandelion/schemas/rsi_event.py new file mode 100644 index 0000000..ca3f985 --- /dev/null +++ b/dandelion/schemas/rsi_event.py @@ -0,0 +1,101 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class RSIEventBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSIEventCreate(BaseModel): + alert_id: Optional[int] = Field(None, alias="alertID", description="Alert ID") + duration: Optional[int] = Field(None, alias="duration", description="Duration") + event_status: Optional[bool] = Field(None, alias="eventStatus", description="Event status") + timestamp: Optional[str] = Field(None, alias="timeStamp", description="Timestamp") + event_class: Optional[str] = Field(None, alias="eventClass", description="Event class") + event_type: Optional[int] = Field(None, alias="eventType", description="Event type") + event_source: Optional[str] = Field(None, alias="eventSource", description="Event source") + event_confidence: Optional[float] = Field( + None, alias="eventConfidence", description="Event confidence" + ) + event_position: Optional[str] = Field( + None, alias="eventPosition", description="Event position" + ) + event_radius: Optional[float] = Field(None, alias="eventRadius", description="Event radius") + event_description: Optional[str] = Field( + None, alias="eventDescription", description="Event description" + ) + event_priority: Optional[int] = Field( + None, alias="eventPriority", description="Event priority" + ) + reference_paths: Optional[str] = Field( + None, alias="referencePaths", description="Reference paths" + ) + area_code: Optional[str] = Field(None, alias="areaCode", description="Area code") + + +# Properties to receive via API on update +class RSIEventUpdate(RSIEventBase): + """""" + + +class RSIEventInDBBase(RSIEventBase): + id: int = Field(..., alias="id", description="RSI Event ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSIEvent(RSIEventInDBBase): + rsu_name: str = Field(..., alias="rsuName", description="RSU name") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU esn") + address: str = Field(..., alias="address", description="RSU address") + event_class: str = Field(..., alias="eventClass", description="Event class") + event_type: int = Field(..., alias="eventType", description="Event type") + create_time: datetime = Field(..., alias="createTime", description="Create time") + country_code: str = Field(..., alias="countryCode", description="Country Code") + country_name: str = Field(..., alias="countryName", description="Country Name") + province_code: str = Field(..., alias="provinceCode", description="Province Code") + province_name: str = Field(..., alias="provinceName", description="Province Name") + city_code: str = Field(..., alias="cityCode", description="City Code") + city_name: str = Field(..., alias="cityName", description="City Name") + area_code: str = Field(..., alias="areaCode", description="Area Code") + area_name: str = Field(..., alias="areaName", description="Area Name") + alert_id: str = Field(..., alias="alertID", description="Alert id") + duration: int = Field(..., alias="duration", description="Duration") + event_status: bool = Field(..., alias="eventStatus", description="Event status") + timestamp: str = Field(..., alias="timestamp", description="Timestamp") + event_source: str = Field(..., alias="eventSource", description="Event source") + event_confidence: float = Field(..., alias="eventConfidence", description="Event confidence") + event_position: Dict[str, Any] = Field( + ..., alias="eventPosition", description="Event position" + ) + event_radius: float = Field(..., alias="eventRadius", description="Event radius") + event_description: str = Field(..., alias="eventDescription", description="Event description") + event_priority: int = Field(..., alias="eventPriority", description="Event priority") + reference_paths: str = Field(..., alias="referencePaths", description="Reference paths") + + +class RSIEvents(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSIEvent] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsm.py b/dandelion/schemas/rsm.py new file mode 100644 index 0000000..5b26bc1 --- /dev/null +++ b/dandelion/schemas/rsm.py @@ -0,0 +1,50 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class RSMBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSMCreate(BaseModel): + ref_pos: Optional[str] = Field(None, alias="refPos", description="Ref pos") + + +# Properties to receive via API on update +class RSMUpdate(RSMBase): + """""" + + +class RSMInDBBase(RSMBase): + id: int = Field(..., alias="id", description="RSM ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSM(RSMInDBBase): + """""" + + +class RSMs(BaseModel): + """""" diff --git a/dandelion/schemas/rsm_participant.py b/dandelion/schemas/rsm_participant.py new file mode 100644 index 0000000..130bf98 --- /dev/null +++ b/dandelion/schemas/rsm_participant.py @@ -0,0 +1,63 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Dict, List + +from pydantic import BaseModel, Field + + +# Shared properties +class RSMParticipantBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSMParticipantCreate(BaseModel): + """""" + + +# Properties to receive via API on update +class RSMParticipantUpdate(RSMParticipantBase): + """""" + + +class RSMParticipantInDBBase(RSMParticipantBase): + id: int = Field(..., alias="id", description="RSM Participant ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSMParticipant(RSMParticipantInDBBase): + ptc_id: int = Field(..., alias="ptcId", description="PTC id") + ptc_type: str = Field(..., alias="ptcType", description="PTC type") + ptc_type_name: str = Field(..., alias="ptcTypeName", description="PTC type name") + source: int = Field(..., alias="source", description="Source") + sec_mark: int = Field(..., alias="secMark", description="Sec mark") + lon: int = Field(..., alias="lon", description="Lon") + lat: int = Field(..., alias="lat", description="Lat") + accuracy: str = Field(..., alias="accuracy", description="Accuracy") + speed: int = Field(..., alias="speed", description="Speed") + heading: int = Field(..., alias="heading", description="Heading") + size: Dict[str, int] = Field(..., alias="size", description="Size") + create_time: datetime = Field(..., alias="createTime", description="Create time") + + +class RSMParticipants(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSMParticipant] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsu.py b/dandelion/schemas/rsu.py new file mode 100644 index 0000000..302b621 --- /dev/null +++ b/dandelion/schemas/rsu.py @@ -0,0 +1,117 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class RSULocation(BaseModel): + lat: Optional[float] = Field(None, alias="lat", description="Latitude") + lon: Optional[float] = Field(None, alias="lon", description="Longitude") + + +class RSUConfigRSUInRSU(BaseModel): + id: int = Field(..., alias="id", description="ID") + status: bool = Field(..., alias="status", description="Status") + rsu_id: int = Field(..., alias="rsu_id", description="RSU ID") + rsu_config_id: int = Field(..., alias="rsu_config_id", description="RSU Config ID") + create_time: datetime = Field(..., alias="create_time", description="Create Time") + + +# Shared properties +class RSUBase(BaseModel): + rsu_id: str = Field(..., alias="rsuId", description="RSU ID") + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + rsu_ip: str = Field(..., alias="rsuIP", description="RSU IP") + country_code: str = Field(..., alias="countryCode", description="Country Code") + country_name: str = Field(..., alias="countryName", description="Country Name") + province_code: str = Field(..., alias="provinceCode", description="Province Code") + province_name: str = Field(..., alias="provinceName", description="Province Name") + city_code: str = Field(..., alias="cityCode", description="City Code") + city_name: str = Field(..., alias="cityName", description="City Name") + area_code: str = Field(..., alias="areaCode", description="Area Code") + area_name: str = Field(..., alias="areaName", description="Area Name") + address: str = Field(..., alias="address", description="Address") + rsu_status: bool = Field(..., alias="rsuStatus", description="RSU Status") + online_status: bool = Field(..., alias="onlineStatus", description="Online Status") + rsu_model_id: Optional[int] = Field(None, alias="rsuModelId", description="RSU Model ID") + desc: Optional[str] = Field(None, alias="desc", description="Description") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + version: Optional[str] = Field(None, alias="version", description="Version") + update_time: datetime = Field(..., alias="updateTime", description="Update Time") + location: Optional[RSULocation] = Field(None, alias="location", description="Location") + + +# Properties to receive via API on creation +class RSUCreate(BaseModel): + tmp_id: Optional[int] = Field(0, alias="tmpId", description="Temporary RSU ID") + rsu_id: str = Field(..., alias="rsuId", description="RSU ID") + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + rsu_ip: str = Field(..., alias="rsuIP", description="RSU IP") + area_code: str = Field(..., alias="areaCode", description="Area Code") + address: str = Field(..., alias="address", description="Address") + rsu_model_id: Optional[int] = Field(None, alias="rsuModelId", description="RSU Model ID") + desc: Optional[str] = Field(None, alias="desc", description="Description") + + +# Properties to receive via API on update +class RSUUpdate(BaseModel): + rsu_id: Optional[str] = Field(None, alias="rsuId", description="RSU ID") + rsu_name: Optional[str] = Field(None, alias="rsuName", description="RSU Name") + rsu_esn: Optional[str] = Field(None, alias="rsuEsn", description="RSU ESN") + rsu_ip: Optional[str] = Field(None, alias="rsuIP", description="RSU IP") + area_code: Optional[str] = Field(None, alias="areaCode", description="Area Code") + address: Optional[str] = Field(None, alias="address", description="Address") + rsu_model_id: Optional[int] = Field(None, alias="rsuModelId", description="RSU Model ID") + desc: Optional[str] = Field(None, alias="desc", description="Description") + rsu_status: Optional[bool] = Field(None, alias="rsuStatus", description="RSU Status") + + +class RSUUpdateWithVersion(BaseModel): + rsu_id: Optional[str] = Field(None, alias="rsuId", description="RSU ID") + rsu_name: Optional[str] = Field(None, alias="rsuName", description="RSU Name") + version: Optional[str] = Field(None, alias="version", description="Version") + location: Optional[RSULocation] = Field(None, alias="location", description="Location") + config: Optional[Dict[str, Any]] = Field(None, alias="config", description="Config") + + +class RSUUpdateWithStatus(RSUUpdate): + online_status: Optional[bool] = Field(None, alias="onlineStatus", description="Status") + + +class RSUInDBBase(RSUBase): + id: int = Field(..., alias="id", description="RSU ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSU(RSUInDBBase): + config: Optional[Dict[str, Any]] = Field(None, alias="config", description="Config") + + +class RSUDetail(RSUInDBBase): + config: List[RSUConfigRSUInRSU] = Field(..., alias="config", description="RSU Config RSU") + + +class RSUs(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSU] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsu_config.py b/dandelion/schemas/rsu_config.py new file mode 100644 index 0000000..e88eb98 --- /dev/null +++ b/dandelion/schemas/rsu_config.py @@ -0,0 +1,130 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Dict, List, Optional + +from pydantic import BaseModel, Field + +from .rsu import RSU + + +class SampleMode(str, Enum): + by_all = "ByAll" + by_id = "ByID" + + +class BSM(BaseModel): + sample_mode: Optional[SampleMode] = Field( + None, alias="sampleMode", description="The sample mode of the BSM." + ) + sample_rate: Optional[int] = Field( + None, alias="sampleRate", description="The sample rate of the BSM." + ) + up_limit: Optional[int] = Field( + None, alias="upLimit", description="The upper limit of the BSM." + ) + up_filters: Optional[List[Dict[str, str]]] = Field( + None, alias="upFilters", description="The upper filters of the BSM." + ) + + +class RSI(BaseModel): + up_filters: Optional[List[Dict[str, str]]] = Field( + None, alias="upFilters", description="The upper filters of the RSI." + ) + + +class RSM(BaseModel): + up_limit: Optional[int] = Field( + None, alias="upLimit", description="The upper limit of the RSM." + ) + up_filters: Optional[List[Dict[str, str]]] = Field( + None, alias="upFilters", description="The upper filters of the RSM." + ) + + +class MAP(BaseModel): + up_limit: Optional[int] = Field( + None, alias="upLimit", description="The upper limit of the MAP." + ) + up_filters: Optional[List[Dict[str, str]]] = Field( + None, alias="upFilters", description="The upper filters of the MAP." + ) + + +class SPAT(BaseModel): + up_limit: Optional[int] = Field( + None, alias="upLimit", description="The upper limit of the SPAT." + ) + up_filters: Optional[List[Dict[str, str]]] = Field( + None, alias="upFilters", description="The upper filters of the SPAT." + ) + + +# Shared properties +class RSUConfigBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSUConfigCreate(BaseModel): + name: str = Field(..., alias="name", description="RSU Config name") + bsm: BSM = Field(..., alias="bsm", description="BSM") + rsi: RSI = Field(..., alias="rsi", description="RSI") + rsm: RSM = Field(..., alias="rsm", description="RSM") + map: MAP = Field(..., alias="map", description="MAP") + spat: Optional[SPAT] = Field(None, alias="spat", description="SPAT") + rsus: Optional[List[int]] = Field(None, alias="rsus", description="RSUs") + + +# Properties to receive via API on update +class RSUConfigUpdate(RSUConfigBase): + name: str = Field(..., alias="name", description="RSU Config name") + bsm: BSM = Field(..., alias="bsm", description="BSM") + rsi: RSI = Field(..., alias="rsi", description="RSI") + rsm: RSM = Field(..., alias="rsm", description="RSM") + map: MAP = Field(..., alias="map", description="MAP") + spat: Optional[SPAT] = Field(None, alias="spat", description="SPAT") + rsus: Optional[List[int]] = Field(None, alias="rsus", description="RSUs") + + +class RSUConfigInDBBase(RSUConfigBase): + id: int = Field(..., alias="id", description="RSU Config ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSUConfig(RSUConfigInDBBase): + name: str = Field(..., alias="name", description="RSU Config name") + bsm_config: Optional[BSM] = Field(None, alias="bsmConfig", description="BSM") + rsi_config: Optional[RSI] = Field(None, alias="rsiConfig", description="RSI") + rsm_config: Optional[RSM] = Field(None, alias="rsmConfig", description="RSM") + map_config: Optional[MAP] = Field(None, alias="mapConfig", description="MAP") + spat_config: Optional[SPAT] = Field(None, alias="spatConfig", description="SPAT") + create_time: datetime = Field(..., alias="createTime", description="Create time") + + +class RSUConfigWithRSUs(RSUConfig): + rsus: List[RSU] = Field(..., alias="rsus", description="RSUs") + + +class RSUConfigs(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSUConfig] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsu_config_rsu.py b/dandelion/schemas/rsu_config_rsu.py new file mode 100644 index 0000000..1ab7802 --- /dev/null +++ b/dandelion/schemas/rsu_config_rsu.py @@ -0,0 +1,48 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +# Shared properties +class RSUConfigRSUBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSUConfigRSUCreate(BaseModel): + """""" + + +# Properties to receive via API on update +class RSUConfigRSUUpdate(RSUConfigRSUBase): + """""" + + +class RSUConfigRSUInDBBase(RSUConfigRSUBase): + id: int = Field(..., alias="id", description="RSU Config RSU ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSUConfigRSU(RSUConfigRSUInDBBase): + """""" + + +class RSUConfigRSUs(BaseModel): + """""" diff --git a/dandelion/schemas/rsu_log.py b/dandelion/schemas/rsu_log.py new file mode 100644 index 0000000..46fac05 --- /dev/null +++ b/dandelion/schemas/rsu_log.py @@ -0,0 +1,80 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import List + +from pydantic import BaseModel, Field + + +class TransProtocal(str, Enum): + http = "http" + https = "https" + ftp = "ftp" + sftp = "sftp" + other = "other" + + +class RSUInRSULog(BaseModel): + id: int = Field(..., alias="id", description="ID") + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + + +# Shared properties +class RSULogBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSULogCreate(BaseModel): + upload_url: str = Field(..., alias="uploadUrl", description="Upload URL") + user_id: str = Field(..., alias="userId", description="User ID") + password: str = Field(..., alias="password", description="Password") + transprotocal: TransProtocal = Field(..., alias="transprotocal", description="Transprotocal") + rsus: List[int] = Field(..., alias="rsus", description="RSUs") + + +# Properties to receive via API on update +class RSULogUpdate(RSULogBase): + upload_url: str = Field(..., alias="uploadUrl", description="Upload URL") + user_id: str = Field(..., alias="userId", description="User ID") + password: str = Field(..., alias="password", description="Password") + transprotocal: TransProtocal = Field(..., alias="transprotocal", description="Transprotocal") + rsus: List[int] = Field(..., alias="rsus", description="RSUs") + + +class RSULogInDBBase(RSULogBase): + id: int = Field(..., alias="id", description="RSU Log ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSULog(RSULogInDBBase): + upload_url: str = Field(..., alias="uploadUrl", description="Upload URL") + user_id: str = Field(..., alias="userId", description="User ID") + password: str = Field(..., alias="password", description="Password") + transprotocal: TransProtocal = Field(..., alias="transprotocal", description="Transprotocal") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + rsus: List[RSUInRSULog] = Field(..., alias="rsus", description="RSUs") + + +class RSULogs(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSULog] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsu_model.py b/dandelion/schemas/rsu_model.py new file mode 100644 index 0000000..093a925 --- /dev/null +++ b/dandelion/schemas/rsu_model.py @@ -0,0 +1,59 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class RSUModelBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSUModelCreate(BaseModel): + name: str = Field(..., alias="name", description="RSU Model Name") + manufacturer: str = Field(..., alias="manufacturer", description="Manufacturer") + desc: Optional[str] = Field("", alias="desc", description="Description") + + +# Properties to receive via API on update +class RSUModelUpdate(RSUModelBase): + name: str = Field(..., alias="name", description="RSU Model Name") + manufacturer: str = Field(..., alias="manufacturer", description="Manufacturer") + desc: Optional[str] = Field("", alias="desc", description="Description") + + +class RSUModelInDBBase(RSUModelBase): + id: int = Field(..., alias="id", description="RSU Model ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSUModel(RSUModelInDBBase): + name: str = Field(..., alias="name", description="RSU Model Name") + manufacturer: str = Field(..., alias="manufacturer", description="Manufacturer") + desc: str = Field(..., alias="desc", description="Description") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + + +class RSUModels(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSUModel] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsu_query.py b/dandelion/schemas/rsu_query.py new file mode 100644 index 0000000..eaa95b6 --- /dev/null +++ b/dandelion/schemas/rsu_query.py @@ -0,0 +1,79 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class RSUInRSUQuery(BaseModel): + rsu_id: int = Field(..., alias="rsuId", description="The ID of the RSU") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + rsu_name: str = Field(..., alias="rsuName", description="RSU name") + + +# Shared properties +class RSUQueryBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSUQueryCreate(BaseModel): + query_type: int = Field(..., alias="queryType", description="Query Type") + time_type: int = Field(..., alias="timeType", description="Time Type") + rsus: List[int] = Field(..., alias="rsus", description="RSUs") + + +# Properties to receive via API on update +class RSUQueryUpdate(RSUQueryBase): + """""" + + +class RSUQueryInDBBase(RSUQueryBase): + id: int = Field(..., alias="id", description="RSU Query ID") + + class Config: + orm_mode = True + + +class RSUQueryDetailBase(BaseModel): + rsu_id: Optional[int] = Field(None, alias="rsuId", description="The ID of the RSU") + rsu_name: str = Field(..., alias="rsuName", description="RSU name") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + query_type: int = Field(..., alias="queryType", description="Query Type") + time_type: int = Field(..., alias="timeType", description="Time Type") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + data: List[Dict[str, Any]] = Field(..., alias="data", description="Data") + + +class RSUQueryDetail(BaseModel): + data: Optional[List[RSUQueryDetailBase]] = Field( + None, alias="data", description="RSU Query Detail" + ) + + +# Additional properties to return via API +class RSUQuery(RSUQueryInDBBase): + query_type: int = Field(..., alias="queryType", description="Query Type") + time_type: int = Field(..., alias="timeType", description="Time Type") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + rsus: List[RSUInRSUQuery] = Field(..., alias="rsus", description="RSUs") + + +class RSUQueries(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSUQuery] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/rsu_query_result.py b/dandelion/schemas/rsu_query_result.py new file mode 100644 index 0000000..8840719 --- /dev/null +++ b/dandelion/schemas/rsu_query_result.py @@ -0,0 +1,49 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +# Shared properties +class RSUQueryResultBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSUQueryResultCreate(BaseModel): + query_id: int = Field(..., description="The ID of the query") + rsu_id: int = Field(..., description="The ID of the RSU") + + +# Properties to receive via API on update +class RSUQueryResultUpdate(RSUQueryResultBase): + """""" + + +class RSUQueryResultInDBBase(RSUQueryResultBase): + id: int = Field(..., alias="id", description="RSU Query Result ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSUQueryResult(RSUQueryResultInDBBase): + """""" + + +class RSUQueryResults(BaseModel): + """""" diff --git a/dandelion/schemas/rsu_tmp.py b/dandelion/schemas/rsu_tmp.py new file mode 100644 index 0000000..415e644 --- /dev/null +++ b/dandelion/schemas/rsu_tmp.py @@ -0,0 +1,62 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class RSUTMPBase(BaseModel): + """""" + + +# Properties to receive via API on creation +class RSUTMPCreate(BaseModel): + rsu_id: Optional[str] = Field(..., alias="rsuId", description="RSU ID") + rsu_name: Optional[str] = Field(..., alias="rsuName", description="RSU Name") + rsu_esn: Optional[str] = Field(..., alias="rsuEsn", description="RSU ESN") + version: Optional[str] = Field(..., alias="version", description="Version") + rsu_status: Optional[str] = Field(..., alias="rsuStatus", description="RSU Status") + location: Optional[Dict[str, float]] = Field(..., alias="location", description="Location") + config: Optional[Dict[str, Any]] = Field(..., alias="config", description="Config") + + +# Properties to receive via API on update +class RSUTMPUpdate(RSUTMPBase): + """""" + + +class RSUTMPInDBBase(RSUTMPBase): + id: int = Field(..., alias="id", description="RSU TMP ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class RSUTMP(RSUTMPInDBBase): + rsu_id: str = Field(..., alias="rsuId", description="RSU ID") + rsu_name: str = Field(..., alias="rsuName", description="RSU Name") + rsu_esn: str = Field(..., alias="rsuEsn", description="RSU ESN") + version: str = Field(..., alias="version", description="Version") + create_time: datetime = Field(..., alias="createTime", description="Create Time") + + +class RSUTMPs(BaseModel): + total: int = Field(..., alias="total", description="Total") + data: List[RSUTMP] = Field(..., alias="data", description="Data") diff --git a/dandelion/schemas/token.py b/dandelion/schemas/token.py new file mode 100644 index 0000000..4fcf29a --- /dev/null +++ b/dandelion/schemas/token.py @@ -0,0 +1,31 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, Field + + +class AccessToken(BaseModel): + access_token: str = Field(..., alias="access_token", description="Access token") + + +class Token(AccessToken): + token_type: str = Field(..., alias="token_type", description="Token type") + + +class TokenPayload(BaseModel): + sub: Optional[int] = Field(None, alias="sub", description="The user ID") diff --git a/dandelion/schemas/user.py b/dandelion/schemas/user.py new file mode 100644 index 0000000..e0d1327 --- /dev/null +++ b/dandelion/schemas/user.py @@ -0,0 +1,48 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, Field + + +# Shared properties +class UserBase(BaseModel): + username: Optional[str] = Field(None, alias="username", description="Username") + is_active: Optional[bool] = Field(True, alias="is_active", description="Is active") + + +# Properties to receive via API on creation +class UserCreate(UserBase): + username: str = Field(..., alias="username", description="Username") + password: str = Field(..., alias="password", description="Password") + + +# Properties to receive via API on update +class UserUpdate(UserBase): + password: Optional[str] = Field(None, alias="password", description="Password") + + +class UserInDBBase(UserBase): + id: Optional[int] = Field(None, alias="id", description="User ID") + + class Config: + orm_mode = True + + +# Additional properties to return via API +class User(UserInDBBase): + """""" diff --git a/dandelion/tests/__init__.py b/dandelion/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dandelion/util/__init__.py b/dandelion/util/__init__.py new file mode 100644 index 0000000..dced28e --- /dev/null +++ b/dandelion/util/__init__.py @@ -0,0 +1,62 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import re + + +class Optional(object): # noqa + def __init__(self, val): + self.val = val + + @staticmethod + def none(val): + return Optional(val) + + def map(self, func): + if self.val is None: + return self + self.val = func(self.val) + return self + + def get(self): + return self.val + + def orElse(self, val_): + if self.val is None: + return val_ + return self.get() + + +def camel2underscore(name): + """Camel to underscore + + >>> camel2underscore('testTESTTestTest0testTEST') + 'test_test_test_test0test_test' + + """ + + name = name[0].upper() + name[1:] + name = re.sub(r"([A-Z][a-z0-9]+)", r"_\1_", name).strip("_").lower() + return re.sub(r"_+", "_", name) + + +def json_to_class(dict_, obj, hump=False): + for v_ in dict_: + key = v_ + if hump: + key = camel2underscore(v_) + obj.__setattr__(key, dict_.get(v_)) + return obj diff --git a/dandelion/version.py b/dandelion/version.py new file mode 100644 index 0000000..4e17772 --- /dev/null +++ b/dandelion/version.py @@ -0,0 +1,25 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pbr import version as pbr_version + +DANDELION_VENDOR = "OpenV2X" +DANDELION_PRODUCT = "Dandelion" +DANDELION_PACKAGE = None # OS distro package version suffix + +loaded = False +version_info = pbr_version.VersionInfo("dandelion") +version_string = version_info.version_string diff --git a/doc/.placeholder b/doc/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/etc/dandelion/dandelion-config-generator.conf b/etc/dandelion/dandelion-config-generator.conf new file mode 100644 index 0000000..e9efbd4 --- /dev/null +++ b/etc/dandelion/dandelion-config-generator.conf @@ -0,0 +1,7 @@ +[DEFAULT] +output_file = etc/dandelion/dandelion.conf.sample +wrap_width = 99 +summarize = true +namespace = dandelion.conf +namespace = oslo.log +namespace = oslo.config diff --git a/etc/dandelion/dandelion.conf.sample b/etc/dandelion/dandelion.conf.sample new file mode 100644 index 0000000..1ab24b5 --- /dev/null +++ b/etc/dandelion/dandelion.conf.sample @@ -0,0 +1,311 @@ +[DEFAULT] + +# +# From oslo.config +# + +# Path to a config file to use. Multiple config files can be specified, with values in later files +# taking precedence. Defaults to %(default)s. This option must be set from the command-line +# (unknown value) +#config_file = ['~/.project/project.conf', '~/project.conf', '/etc/project/project.conf', '/etc/project.conf'] + +# Path to a config directory to pull `*.conf` files from. This file set is sorted, so as to provide +# a predictable parse order if individual options are over-ridden. The set is parsed after the +# file(s) specified via previous --config-file, arguments hence over-ridden options in the +# directory take precedence. This option must be set from the command-line (list value) +#config_dir = ~/.project/project.conf.d/,~/project.conf.d/,/etc/project/project.conf.d/,/etc/project.conf.d/ + +# Lists configuration groups that provide more details for accessing configuration settings from +# locations other than local files (list value) +#config_source = + +# +# From oslo.log +# + +# If set to true, the logging level will be set to DEBUG instead of the default INFO level (boolean +# value) +# Note: This option can be changed without restarting. +#debug = false + +# The name of a logging configuration file. This file is appended to any existing logging +# configuration files. For details about logging configuration files, see the Python logging module +# documentation. Note that when logging configuration files are used then all logging configuration +# is set in the configuration file and other logging configuration options are ignored (for +# example, log-date-format) (string value) +# Note: This option can be changed without restarting. +# Deprecated group/name - [DEFAULT]/log_config +#log_config_append = + +# Defines the format string for %%(asctime)s in log records. Default: %(default)s . This option is +# ignored if log_config_append is set (string value) +#log_date_format = %Y-%m-%d %H:%M:%S + +# (Optional) Name of log file to send logging output to. If no default is set, logging will go to +# stderr as defined by use_stderr. This option is ignored if log_config_append is set (string +# value) +# Deprecated group/name - [DEFAULT]/logfile +#log_file = + +# (Optional) The base directory used for relative log_file paths. This option is ignored if +# log_config_append is set (string value) +# Deprecated group/name - [DEFAULT]/logdir +#log_dir = + +# Uses logging handler designed to watch file system. When log file is moved or removed this +# handler will open a new log file with specified path instantaneously. It makes sense only if +# log_file option is specified and Linux platform is used. This option is ignored if +# log_config_append is set (boolean value) +#watch_log_file = false + +# Use syslog for logging. Existing syslog format is DEPRECATED and will be changed later to honor +# RFC5424. This option is ignored if log_config_append is set (boolean value) +#use_syslog = false + +# Enable journald for logging. If running in a systemd environment you may wish to enable journal +# support. Doing so will use the journal native protocol which includes structured metadata in +# addition to log messages.This option is ignored if log_config_append is set (boolean value) +#use_journal = false + +# Syslog facility to receive log lines. This option is ignored if log_config_append is set (string +# value) +#syslog_log_facility = LOG_USER + +# Use JSON formatting for logging. This option is ignored if log_config_append is set (boolean +# value) +#use_json = false + +# Log output to standard error. This option is ignored if log_config_append is set (boolean value) +#use_stderr = false + +# Log output to Windows Event Log (boolean value) +#use_eventlog = false + +# The amount of time before the log files are rotated. This option is ignored unless +# log_rotation_type is set to "interval" (integer value) +#log_rotate_interval = 1 + +# Rotation interval type. The time of the last file change (or the time when the service was +# started) is used when scheduling the next rotation (string value) +# Possible values: +# Seconds - +# Minutes - +# Hours - +# Days - +# Weekday - +# Midnight - +#log_rotate_interval_type = days + +# Maximum number of rotated log files (integer value) +#max_logfile_count = 30 + +# Log file maximum size in MB. This option is ignored if "log_rotation_type" is not set to "size" +# (integer value) +#max_logfile_size_mb = 200 + +# Log rotation type (string value) +# Possible values: +# interval - Rotate logs at predefined time intervals. +# size - Rotate logs once they reach a predefined size. +# none - Do not rotate log files. +#log_rotation_type = none + +# Format string to use for log messages with context. Used by oslo_log.formatters.ContextFormatter +# (string value) +#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(global_request_id)s %(request_id)s %(user_identity)s] %(instance)s%(message)s + +# Format string to use for log messages when context is undefined. Used by +# oslo_log.formatters.ContextFormatter (string value) +#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s + +# Additional data to append to log message when logging level for the message is DEBUG. Used by +# oslo_log.formatters.ContextFormatter (string value) +#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d + +# Prefix each line of exception output with this format. Used by +# oslo_log.formatters.ContextFormatter (string value) +#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s + +# Defines the format string for %(user_identity)s that is used in logging_context_format_string. +# Used by oslo_log.formatters.ContextFormatter (string value) +#logging_user_identity_format = %(user)s %(project)s %(domain)s %(system_scope)s %(user_domain)s %(project_domain)s + +# List of package logging levels in logger=LEVEL pairs. This option is ignored if log_config_append +# is set (list value) +#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,oslo_messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN,oslo.cache=INFO,oslo_policy=INFO,dogpile.core.dogpile=INFO + +# Enables or disables publication of error events (boolean value) +#publish_errors = false + +# The format for an instance that is passed with the log message (string value) +#instance_format = "[instance: %(uuid)s] " + +# The format for an instance UUID that is passed with the log message (string value) +#instance_uuid_format = "[instance: %(uuid)s] " + +# Interval, number of seconds, of log rate limiting (integer value) +#rate_limit_interval = 0 + +# Maximum number of logged messages per rate_limit_interval (integer value) +#rate_limit_burst = 0 + +# Log level name used by rate limiting: CRITICAL, ERROR, INFO, WARNING, DEBUG or empty string. Logs +# with level greater or equal to rate_limit_except_level are not filtered. An empty string means +# that all levels are filtered (string value) +#rate_limit_except_level = CRITICAL + +# Enables or disables fatal status of deprecations (boolean value) +#fatal_deprecations = false + + +[cors] +# +# CORS related options. + +# +# From dandelion.conf +# + +# +# CORS origins. +# (list value) +#origins = + + +[database] +# +# Database related options. + +# +# From dandelion.conf +# + +# +# Connection of database. +# (string value) +#connection = sqlite:////tmp/dandelion.db + + +[mqtt] +# +# MQTT related options. + +# +# From dandelion.conf +# + +# +# Host of MQTT server. +# (IP address value) +#host = + +# +# Port of MQTT server. +# (port value) +# Minimum value: 0 +# Maximum value: 65535 +#port = 1883 + +# +# Username of MQTT server. +# (string value) +#username = + +# +# Password for username of MQTT server. +# (string value) +#password = + + +[redis] +# +# Redis related options. + +# +# From dandelion.conf +# + +# +# Connection of redis. +# If you have a single redis server, you can set connection as followed: +# "redis://:@:?db=0&socket_timeout=60&retry_on_timeout=yes". +# If you have a sentinel redis cluster, you can set connection as followed: +# "redis://:@:?sentinel=&sentinel_fallback=:&sentinel_fallback=:&db=0&socket_timeout=60&retry_on_timeout=yes" +# (string value) +#connection = + + +[sample_remote_file_source] +# Example of using a remote_file source +# +# remote_file: A backend driver for remote files served through http[s]. +# +# Required options: +# - uri: URI containing the file location. +# +# Non-required options: +# - ca_path: The path to a CA_BUNDLE file or directory with +# certificates of trusted CAs. +# +# - client_cert: Client side certificate, as a single file path +# containing either the certificate only or the +# private key and the certificate. +# +# - client_key: Client side private key, in case client_cert is +# specified but does not includes the private key. + +# +# From oslo.config +# + +# The name of the driver that can load this configuration source (string value) +# +# This option has a sample default set, which means that +# its actual default value may vary from the one documented +# below. +#driver = remote_file + +# Required option with the URI of the extra configuration file's location (uri value) +# +# This option has a sample default set, which means that +# its actual default value may vary from the one documented +# below. +#uri = https://example.com/my-configuration.ini + +# The path to a CA_BUNDLE file or directory with certificates of trusted CAs (string value) +# +# This option has a sample default set, which means that +# its actual default value may vary from the one documented +# below. +#ca_path = /etc/ca-certificates + +# Client side certificate, as a single file path containing either the certificate only or the +# private key and the certificate (string value) +# +# This option has a sample default set, which means that +# its actual default value may vary from the one documented +# below. +#client_cert = /etc/ca-certificates/service-client-keystore + +# Client side private key, in case client_cert is specified but does not includes the private key +# (string value) +#client_key = + + +[token] +# +# Token related options. + +# +# From dandelion.conf +# + +# +# Token expire seonds. Default: 604800(7 days). +# (integer value) +#expire_seconds = 604800 + +# +# Secret key of token. +# (string value) +#secret_key = CP7l45i1SEk7jues8DAcO3MnWe-NMKITz3XrMxHBZhY diff --git a/etc/dandelion/gunicorn.py b/etc/dandelion/gunicorn.py new file mode 100644 index 0000000..d8932b7 --- /dev/null +++ b/etc/dandelion/gunicorn.py @@ -0,0 +1,66 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +bind = ["0.0.0.0:28300"] +# bind = "unix://tmp/dandelion.sock" +workers = 1 +worker_class = "uvicorn.workers.UvicornWorker" +timeout = 300 +keepalive = 5 +reuse_port = True +proc_name = "dandelion" + +logconfig_dict = { + "version": 1, + "disable_existing_loggers": False, + "root": {"level": "DEBUG", "handlers": ["console"]}, + "loggers": { + "gunicorn.error": { + "level": "DEBUG", + "handlers": ["error_file"], + "propagate": 0, + "qualname": "gunicorn_error", + }, + "gunicorn.access": { + "level": "DEBUG", + "handlers": ["access_file"], + "propagate": 0, + "qualname": "access", + }, + }, + "handlers": { + "error_file": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "generic", + "filename": "/var/log/dandelion/error.log", + }, + "access_file": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "generic", + "filename": "/var/log/dandelion/access.log", + }, + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "generic", + }, + }, + "formatters": { + "generic": { + "format": "%(asctime)s.%(msecs)03d %(process)d %(levelname)s [-] %(message)s", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter", + } + }, +} diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..3eded94 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,47 @@ +# https://mypy.readthedocs.io/en/stable/config_file.html +[mypy] +# Import discovery +ignore_missing_imports = true +follow_imports = normal + +# Platform configuration + +# Disallow dynamic typing + +# Untyped definitions and calls +check_untyped_defs = true + +# None and Optional handling +no_implicit_optional = true +strict_optional = true + +# Configuring warnings +show_error_context = true +show_column_numbers = true +warn_unused_ignores = true + +# Suppressing errors + +# Miscellaneous strictness flags + +# Configuring error messages +show_error_codes = true +pretty = true +color_output = true +error_summary = true +show_absolute_path = false + +# Incremental mode +incremental = true +cache_dir = .mypy_cache +sqlite_cache = false +cache_fine_grained = false +skip_version_check = false +skip_cache_mtime_checks = false + +# Advanced options + +# Report generation +html_report = mypy-report + +# Miscellaneous diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..92393c2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,21 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +fastapi>=0.78.0 # MIT +fastapi-utils>=0.2.1 # MIT +oslo.config>=8.8.0 # Apache-2.0 +oslo.log>=5.0.0 # Apache-2.0 +alembic>=1.8.0 # MIT +pydantic>=1.9.1 # MIT +PyMySQL>=1.0.2 # MIT +SQLAlchemy>=1.4.37 # MIT +uvicorn>=0.17.6 # BSD +gunicorn>=20.1.0 # MIT +paho-mqtt>=1.6.1 # OSI-Approved +redis>=4.3.3 # MIT +types-redis>=4.2.7 # Apache-2.0 +python-jose>=3.3.0 # MIT +passlib>=1.7.4 # BSD +python-multipart>=0.0.5 # Apache-2.0 +bcrypt>=3.2.2 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..14d0191 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,29 @@ +[metadata] +name = dandelion +summary = OpenV2X Device Management APIServer +description_file = +author = 99cloud +author_email = 99cloud@99cloud.net +home_page = +python_requires = >=3.8 +classifier = + Environment :: OpenV2X + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + +[files] +packages = + dandelion + dandelion/alembic + +[entry_points] +oslo.config.opts = + dandelion.conf = dandelion.conf.opts:list_opts diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..821bd06 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import setuptools + +setuptools.setup(setup_requires=["pbr>=2.0.0"], pbr=True) diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..64ead83 --- /dev/null +++ b/swagger.json @@ -0,0 +1,8887 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Dandelion - OpenV2X Device Management - APIServer", + "version": "0.1.0" + }, + "paths": { + "/api/v1/login": { + "post": { + "tags": [ + "User" + ], + "summary": "Login", + "description": "\nUser login with username and password.\n", + "operationId": "login_api_v1_login_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_login_api_v1_login_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/login/access-token": { + "post": { + "tags": [ + "User" + ], + "summary": "Login Access Token(DO NOT USE IN PRODUCTION)", + "description": "\n- `DO NOT USE IN PRODUCTION !!!`\n- `JUST FOR TESTING PURPOSE !!!`\n", + "operationId": "access_token_api_v1_login_access_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_access_token_api_v1_login_access_token_post" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/users": { + "post": { + "tags": [ + "User" + ], + "summary": "Create", + "description": "Create new user.", + "operationId": "create_api_v1_users_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/users/me": { + "get": { + "tags": [ + "User" + ], + "summary": "Get", + "description": "\nGet detailed info of me(current login user).\n", + "operationId": "get_api_v1_users_me_get", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/countries": { + "get": { + "tags": [ + "Area" + ], + "summary": "List", + "description": "\nGet all countries list.\n", + "operationId": "list_api_v1_countries_get", + "parameters": [ + { + "description": "Cascade to list all countries.", + "required": false, + "schema": { + "title": "Cascade", + "type": "boolean", + "description": "Cascade to list all countries." + }, + "name": "cascade", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "Response 200 List Api V1 Countries Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Country" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/provinces": { + "get": { + "tags": [ + "Area" + ], + "summary": "List", + "description": "\nSearch province by country.\n", + "operationId": "list_api_v1_provinces_get", + "parameters": [ + { + "description": "Filter by countryCode", + "required": true, + "schema": { + "title": "Countrycode", + "type": "string", + "description": "Filter by countryCode" + }, + "name": "countryCode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "Response 200 List Api V1 Provinces Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Province" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/cities": { + "get": { + "tags": [ + "Area" + ], + "summary": "List", + "description": "\nSearch city by province.\n", + "operationId": "list_api_v1_cities_get", + "parameters": [ + { + "description": "Filter by provinceCode", + "required": true, + "schema": { + "title": "Provincecode", + "type": "string", + "description": "Filter by provinceCode" + }, + "name": "provinceCode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "Response 200 List Api V1 Cities Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/City" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/areas": { + "get": { + "tags": [ + "Area" + ], + "summary": "List", + "description": "\nSearch area by city.\n", + "operationId": "list_api_v1_areas_get", + "parameters": [ + { + "description": "Filter by cityCode", + "required": true, + "schema": { + "title": "Citycode", + "type": "string", + "description": "Filter by cityCode" + }, + "name": "cityCode", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "Response 200 List Api V1 Areas Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Area" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/cameras": { + "get": { + "tags": [ + "Camera" + ], + "summary": "List", + "description": "\nGet all Cameras.\n", + "operationId": "list_api_v1_cameras_get", + "parameters": [ + { + "description": "Filter by camera sn. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Sn", + "type": "string", + "description": "Filter by camera sn. Fuzzy prefix query is supported" + }, + "name": "sn", + "in": "query" + }, + { + "description": "Filter by camera name. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Name", + "type": "string", + "description": "Filter by camera name. Fuzzy prefix query is supported" + }, + "name": "name", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Cameras" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "Camera" + ], + "summary": "Create", + "description": "\nCreate a new Camera.\n", + "operationId": "create_api_v1_cameras_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CameraCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Camera" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/cameras/{camera_id}": { + "get": { + "tags": [ + "Camera" + ], + "summary": "Get", + "description": "\nGet a Camera.\n", + "operationId": "get_api_v1_cameras__camera_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Camera Id", + "type": "integer" + }, + "name": "camera_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Camera" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "put": { + "tags": [ + "Camera" + ], + "summary": "Update", + "description": "\nUpdate a Camera.\n", + "operationId": "update_api_v1_cameras__camera_id__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Camera Id", + "type": "integer" + }, + "name": "camera_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CameraUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Camera" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "delete": { + "tags": [ + "Camera" + ], + "summary": "Delete", + "description": "\nDelete a Camera.\n", + "operationId": "delete_api_v1_cameras__camera_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Camera Id", + "type": "integer" + }, + "name": "camera_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/homes/online_rate": { + "get": { + "tags": [ + "Cloud Control Home" + ], + "summary": "Online Rate", + "description": "\nGet online rate of all devices.\n", + "operationId": "online_rate_api_v1_homes_online_rate_get", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnlineRate" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/homes/route_info": { + "get": { + "tags": [ + "Cloud Control Home" + ], + "summary": "Route Info", + "description": "\nGet traffic situation.\n", + "operationId": "route_info_api_v1_homes_route_info_get", + "parameters": [ + { + "description": "RSU ESN", + "required": true, + "schema": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "name": "rsuEsn", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouteInfo" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/homes/route_info_push": { + "post": { + "tags": [ + "Cloud Control Home" + ], + "summary": "Route Info Push", + "description": "\nPush traffic situation.\n", + "operationId": "route_info_push_api_v1_homes_route_info_push_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Route Info In", + "allOf": [ + { + "$ref": "#/components/schemas/RouteInfoCreate" + } + ], + "description": "Route Info" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RouteInfo" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/maps/{map_id}/rsus": { + "get": { + "tags": [ + "Map" + ], + "summary": "List", + "description": "\nGet all Map RSUs.\n", + "operationId": "list_api_v1_maps__map_id__rsus_get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Map Id", + "type": "integer" + }, + "name": "map_id", + "in": "path" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MapRSUs" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "Map" + ], + "summary": "Create", + "description": "\nCreate a new Radar.\n", + "operationId": "create_api_v1_maps__map_id__rsus_post", + "parameters": [ + { + "required": true, + "schema": { + "title": "Map Id", + "type": "integer" + }, + "name": "map_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MapRSUCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MapRSU" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/maps/{map_id}/rsus/{map_rsu_id}": { + "delete": { + "tags": [ + "Map" + ], + "summary": "Delete", + "description": "\nDelete a Map RSU.\n", + "operationId": "delete_api_v1_maps__map_id__rsus__map_rsu_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Map Id", + "type": "integer" + }, + "name": "map_id", + "in": "path" + }, + { + "required": true, + "schema": { + "title": "Map Rsu Id", + "type": "integer" + }, + "name": "map_rsu_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/maps": { + "get": { + "tags": [ + "Map" + ], + "summary": "List", + "description": "\nGet all Maps.\n", + "operationId": "list_api_v1_maps_get", + "parameters": [ + { + "description": "Filter by map name. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Name", + "type": "string", + "description": "Filter by map name. Fuzzy prefix query is supported" + }, + "name": "name", + "in": "query" + }, + { + "description": "Filter by map areaCode", + "required": false, + "schema": { + "title": "Areacode", + "type": "string", + "description": "Filter by map areaCode" + }, + "name": "areaCode", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Maps" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "Map" + ], + "summary": "Create", + "description": "\nCreate a new Map.\n", + "operationId": "create_api_v1_maps_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MapCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Map" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/maps/{map_id}": { + "get": { + "tags": [ + "Map" + ], + "summary": "Get", + "description": "\nGet a Radar.\n", + "operationId": "get_api_v1_maps__map_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Map Id", + "type": "integer" + }, + "name": "map_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Map" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "put": { + "tags": [ + "Map" + ], + "summary": "Update", + "description": "\nUpdate a Map.\n", + "operationId": "update_api_v1_maps__map_id__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Map Id", + "type": "integer" + }, + "name": "map_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MapUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Map" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "delete": { + "tags": [ + "Map" + ], + "summary": "Delete", + "description": "\nDelete a Map.\n", + "operationId": "delete_api_v1_maps__map_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Map Id", + "type": "integer" + }, + "name": "map_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/maps/{map_id}/data": { + "get": { + "tags": [ + "Map" + ], + "summary": "Data", + "description": "\nGet a Map data.\n", + "operationId": "data_api_v1_maps__map_id__data_get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Map Id", + "type": "integer" + }, + "name": "map_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "Response 200 Data Api V1 Maps Map Id Data Get", + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/mngs": { + "get": { + "tags": [ + "MNG" + ], + "summary": "List", + "description": "\nGet all MNGs.\n", + "operationId": "list_api_v1_mngs_get", + "parameters": [ + { + "description": "Filter by rsuName. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Rsuname", + "type": "string", + "description": "Filter by rsuName. Fuzzy prefix query is supported" + }, + "name": "rsuName", + "in": "query" + }, + { + "description": "Filter by rsuEsn", + "required": false, + "schema": { + "title": "Rsuesn", + "type": "string", + "description": "Filter by rsuEsn" + }, + "name": "rsuEsn", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MNGs" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/mngs/{mng_id}": { + "put": { + "tags": [ + "MNG" + ], + "summary": "Update", + "description": "\nUpdate a MNG.\n", + "operationId": "update_api_v1_mngs__mng_id__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Mng Id", + "type": "integer" + }, + "name": "mng_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MNGUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MNG" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/mngs/{mng_id}/down": { + "post": { + "tags": [ + "MNG" + ], + "summary": "Down", + "description": "\nDown a MNG.\n", + "operationId": "down_api_v1_mngs__mng_id__down_post", + "parameters": [ + { + "required": true, + "schema": { + "title": "Mng Id", + "type": "integer" + }, + "name": "mng_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/mngs/{mng_id}/copy": { + "post": { + "tags": [ + "MNG" + ], + "summary": "Copy", + "description": "\nCopy a MNG.\n", + "operationId": "copy_api_v1_mngs__mng_id__copy_post", + "parameters": [ + { + "required": true, + "schema": { + "title": "Mng Id", + "type": "integer" + }, + "name": "mng_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Mng Copy In", + "allOf": [ + { + "$ref": "#/components/schemas/MNGCopy" + } + ], + "description": "MNG copy" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/radars": { + "get": { + "tags": [ + "Radar" + ], + "summary": "List", + "description": "\nGet all Radars.\n", + "operationId": "list_api_v1_radars_get", + "parameters": [ + { + "description": "Filter by radar sn. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Sn", + "type": "string", + "description": "Filter by radar sn. Fuzzy prefix query is supported" + }, + "name": "sn", + "in": "query" + }, + { + "description": "Filter by radar name. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Name", + "type": "string", + "description": "Filter by radar name. Fuzzy prefix query is supported" + }, + "name": "name", + "in": "query" + }, + { + "description": "Filter by rsuId", + "required": false, + "schema": { + "title": "Rsuid", + "type": "integer", + "description": "Filter by rsuId" + }, + "name": "rsuId", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Radars" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "Radar" + ], + "summary": "Create", + "description": "\nCreate a new Radar.\n", + "operationId": "create_api_v1_radars_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RadarCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Radar" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/radars/{radar_id}": { + "get": { + "tags": [ + "Radar" + ], + "summary": "Get", + "description": "\nGet a Radar.\n", + "operationId": "get_api_v1_radars__radar_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Radar Id", + "type": "integer" + }, + "name": "radar_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Radar" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "put": { + "tags": [ + "Radar" + ], + "summary": "Update", + "description": "\nUpdate a Radar.\n", + "operationId": "update_api_v1_radars__radar_id__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Radar Id", + "type": "integer" + }, + "name": "radar_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RadarUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Radar" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "delete": { + "tags": [ + "Radar" + ], + "summary": "Delete", + "description": "\nDelete a Radar.\n", + "operationId": "delete_api_v1_radars__radar_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Radar Id", + "type": "integer" + }, + "name": "radar_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/events/{event_id}": { + "get": { + "tags": [ + "Event" + ], + "summary": "Get", + "description": "\nGet a RSIEvent.\n", + "operationId": "get_api_v1_events__event_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Event Id", + "type": "integer" + }, + "name": "event_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSIEvent" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/events": { + "get": { + "tags": [ + "Event" + ], + "summary": "List", + "description": "\nGet all RSI Events.\n", + "operationId": "list_api_v1_events_get", + "parameters": [ + { + "description": "Filter by eventType", + "required": false, + "schema": { + "title": "Eventtype", + "type": "integer", + "description": "Filter by eventType" + }, + "name": "eventType", + "in": "query" + }, + { + "description": "Filter by areaCode", + "required": false, + "schema": { + "title": "Areacode", + "type": "string", + "description": "Filter by areaCode" + }, + "name": "areaCode", + "in": "query" + }, + { + "description": "Filter by address. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Address", + "type": "string", + "description": "Filter by address. Fuzzy prefix query is supported" + }, + "name": "address", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSIEvents" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsms": { + "get": { + "tags": [ + "RSM" + ], + "summary": "List", + "description": "\nGet all RSM.\n", + "operationId": "list_api_v1_rsms_get", + "parameters": [ + { + "description": "Filter by ptcType", + "required": false, + "schema": { + "title": "Ptctype", + "type": "integer", + "description": "Filter by ptcType" + }, + "name": "ptcType", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSMParticipants" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_configs": { + "get": { + "tags": [ + "RSU Config" + ], + "summary": "List", + "description": "\nGet all RSUConfigs.\n", + "operationId": "list_api_v1_rsu_configs_get", + "parameters": [ + { + "description": "Filter by name. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Name", + "type": "string", + "description": "Filter by name. Fuzzy prefix query is supported" + }, + "name": "name", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUConfigs" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "RSU Config" + ], + "summary": "Create", + "description": "\nCreate a new RSU Config.\n", + "operationId": "create_api_v1_rsu_configs_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUConfigCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUConfig" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_configs/{rsu_config_id}": { + "get": { + "tags": [ + "RSU Config" + ], + "summary": "Get", + "description": "\nGet a RSUConfig.\n", + "operationId": "get_api_v1_rsu_configs__rsu_config_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Config Id", + "type": "integer" + }, + "name": "rsu_config_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUConfigWithRSUs" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "put": { + "tags": [ + "RSU Config" + ], + "summary": "Update", + "description": "\nUpdate a Radar.\n", + "operationId": "update_api_v1_rsu_configs__rsu_config_id__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Config Id", + "type": "integer" + }, + "name": "rsu_config_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUConfigUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUConfig" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "delete": { + "tags": [ + "RSU Config" + ], + "summary": "Delete", + "description": "\nDelete a RSUConfig.\n", + "operationId": "delete_api_v1_rsu_configs__rsu_config_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Config Id", + "type": "integer" + }, + "name": "rsu_config_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_logs": { + "get": { + "tags": [ + "RSU Log" + ], + "summary": "List", + "description": "\nGet all RSULogs.\n", + "operationId": "list_api_v1_rsu_logs_get", + "parameters": [ + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSULogs" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "RSU Log" + ], + "summary": "Create", + "description": "\nCreate a new RSU log.\n", + "operationId": "create_api_v1_rsu_logs_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSULogCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSULog" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_logs/{rsu_log_id}": { + "get": { + "tags": [ + "RSU Log" + ], + "summary": "Get", + "description": "\nGet a RSULog.\n", + "operationId": "get_api_v1_rsu_logs__rsu_log_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Log Id", + "type": "integer" + }, + "name": "rsu_log_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSULog" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "put": { + "tags": [ + "RSU Log" + ], + "summary": "Update", + "description": "\nUpdate a RSULog.\n", + "operationId": "update_api_v1_rsu_logs__rsu_log_id__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Log Id", + "type": "integer" + }, + "name": "rsu_log_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSULogUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSULog" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "delete": { + "tags": [ + "RSU Log" + ], + "summary": "Delete", + "description": "\nDelete a RSULog.\n", + "operationId": "delete_api_v1_rsu_logs__rsu_log_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Log Id", + "type": "integer" + }, + "name": "rsu_log_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_models": { + "get": { + "tags": [ + "RSU Model" + ], + "summary": "List", + "description": "\nGet all RSU Models.\n", + "operationId": "list_api_v1_rsu_models_get", + "parameters": [ + { + "description": "Filter by name. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Name", + "type": "string", + "description": "Filter by name. Fuzzy prefix query is supported" + }, + "name": "name", + "in": "query" + }, + { + "description": "Filter by manufacturer. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Manufacturer", + "type": "string", + "description": "Filter by manufacturer. Fuzzy prefix query is supported" + }, + "name": "manufacturer", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUModels" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "RSU Model" + ], + "summary": "Create", + "description": "\nCreate a new RSU Model.\n", + "operationId": "create_api_v1_rsu_models_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUModelCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUModel" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_models/{rsu_model_id}": { + "get": { + "tags": [ + "RSU Model" + ], + "summary": "Get", + "description": "\nGet a RSU Model.\n", + "operationId": "get_api_v1_rsu_models__rsu_model_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Model Id", + "type": "integer" + }, + "name": "rsu_model_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUModel" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "put": { + "tags": [ + "RSU Model" + ], + "summary": "Update", + "description": "\nUpdate a RSU Model.\n", + "operationId": "update_api_v1_rsu_models__rsu_model_id__put", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Model Id", + "type": "integer" + }, + "name": "rsu_model_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUModelUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUModel" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "delete": { + "tags": [ + "RSU Model" + ], + "summary": "Delete", + "description": "\nDelete a RSU Model.\n", + "operationId": "delete_api_v1_rsu_models__rsu_model_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Model Id", + "type": "integer" + }, + "name": "rsu_model_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_queries": { + "get": { + "tags": [ + "RSU Query" + ], + "summary": "List", + "description": "\nGet all RSUQueries.\n", + "operationId": "list_api_v1_rsu_queries_get", + "parameters": [ + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUQueries" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "RSU Query" + ], + "summary": "Create", + "description": "\nCreate a new RSU Query.\n", + "operationId": "create_api_v1_rsu_queries_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUQueryCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUQuery" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_queries/{rsu_query_id}": { + "get": { + "tags": [ + "RSU Query" + ], + "summary": "Get", + "description": "\nGet a RSU Query.\n", + "operationId": "get_api_v1_rsu_queries__rsu_query_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Query Id", + "type": "integer" + }, + "name": "rsu_query_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUQueryDetail" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsu_tmps": { + "get": { + "tags": [ + "RSU TMP" + ], + "summary": "List", + "description": "\nGet all TMP RSUs.\n", + "operationId": "list_api_v1_rsu_tmps_get", + "parameters": [ + { + "description": "Filter by rsuName. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Rsuname", + "type": "string", + "description": "Filter by rsuName. Fuzzy prefix query is supported" + }, + "name": "rsuName", + "in": "query" + }, + { + "description": "Filter by rsuEsn", + "required": false, + "schema": { + "title": "Rsuesn", + "type": "string", + "description": "Filter by rsuEsn" + }, + "name": "rsuEsn", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUTMPs" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsus": { + "get": { + "tags": [ + "RSU" + ], + "summary": "List", + "description": "\nGet all RSUs.\n", + "operationId": "list_api_v1_rsus_get", + "parameters": [ + { + "description": "Filter by rsuName. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Rsuname", + "type": "string", + "description": "Filter by rsuName. Fuzzy prefix query is supported" + }, + "name": "rsuName", + "in": "query" + }, + { + "description": "Filter by rsuEsn. Fuzzy prefix query is supported", + "required": false, + "schema": { + "title": "Rsuesn", + "type": "string", + "description": "Filter by rsuEsn. Fuzzy prefix query is supported" + }, + "name": "rsuEsn", + "in": "query" + }, + { + "description": "Filter by areaCode", + "required": false, + "schema": { + "title": "Areacode", + "type": "string", + "description": "Filter by areaCode" + }, + "name": "areaCode", + "in": "query" + }, + { + "description": "Filter by onlineStatus", + "required": false, + "schema": { + "title": "Onlinestatus", + "type": "boolean", + "description": "Filter by onlineStatus" + }, + "name": "onlineStatus", + "in": "query" + }, + { + "description": "Filter by rsuStatus", + "required": false, + "schema": { + "title": "Rsustatus", + "type": "boolean", + "description": "Filter by rsuStatus" + }, + "name": "rsuStatus", + "in": "query" + }, + { + "description": "Page number", + "required": false, + "schema": { + "title": "Pagenum", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page number", + "default": 1 + }, + "name": "pageNum", + "in": "query" + }, + { + "description": "Page size", + "required": false, + "schema": { + "title": "Pagesize", + "exclusiveMinimum": 0.0, + "type": "integer", + "description": "Page size", + "default": 10 + }, + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUs" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "post": { + "tags": [ + "RSU" + ], + "summary": "Create", + "description": "\nCreate a new RSU.\n", + "operationId": "create_api_v1_rsus_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSU" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsus/{rsu_id}": { + "get": { + "tags": [ + "RSU" + ], + "summary": "Get", + "description": "\nGet a RSU.\n", + "operationId": "get_api_v1_rsus__rsu_id__get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Id", + "type": "integer" + }, + "name": "rsu_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUDetail" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "delete": { + "tags": [ + "RSU" + ], + "summary": "Delete", + "description": "\nDelete a RSU.\n", + "operationId": "delete_api_v1_rsus__rsu_id__delete", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Id", + "type": "integer" + }, + "name": "rsu_id", + "in": "path" + } + ], + "responses": { + "204": { + "description": "No Content", + "class": { + "__module__": "starlette.responses", + "media_type": "application/json", + "__init__": {}, + "render": {} + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + }, + "patch": { + "tags": [ + "RSU" + ], + "summary": "Update", + "description": "\nUpdate a RSU.\n", + "operationId": "update_api_v1_rsus__rsu_id__patch", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Id", + "type": "integer" + }, + "name": "rsu_id", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSUUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSU" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsus/{rsu_esn}/location": { + "get": { + "tags": [ + "RSU" + ], + "summary": "Get Location", + "description": "\nGet a RSU Location.\n", + "operationId": "get_location_api_v1_rsus__rsu_esn__location_get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Esn", + "type": "string" + }, + "name": "rsu_esn", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RSULocation" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/v1/rsus/{rsu_id}/map": { + "get": { + "tags": [ + "RSU" + ], + "summary": "Get Map", + "description": "\nGet a RSU Map.\n", + "operationId": "get_map_api_v1_rsus__rsu_id__map_get", + "parameters": [ + { + "required": true, + "schema": { + "title": "Rsu Id", + "type": "integer" + }, + "name": "rsu_id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "title": "Response 200 Get Map Api V1 Rsus Rsu Id Map Get", + "type": "object" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorMessage" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + } + }, + "components": { + "schemas": { + "AddressChg": { + "title": "AddressChg", + "required": [ + "cssUrl", + "time" + ], + "type": "object", + "properties": { + "cssUrl": { + "title": "Cssurl", + "type": "string", + "description": "CSS URL" + }, + "time": { + "title": "Time", + "type": "integer", + "description": "Time" + } + } + }, + "Area": { + "title": "Area", + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "code": { + "title": "Code", + "type": "string", + "description": "Area code" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Area name" + } + } + }, + "BSM": { + "title": "BSM", + "type": "object", + "properties": { + "sampleMode": { + "allOf": [ + { + "$ref": "#/components/schemas/SampleMode" + } + ], + "description": "The sample mode of the BSM." + }, + "sampleRate": { + "title": "Samplerate", + "type": "integer", + "description": "The sample rate of the BSM." + }, + "upLimit": { + "title": "Uplimit", + "type": "integer", + "description": "The upper limit of the BSM." + }, + "upFilters": { + "title": "Upfilters", + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": "The upper filters of the BSM." + } + } + }, + "Body_access_token_api_v1_login_access_token_post": { + "title": "Body_access_token_api_v1_login_access_token_post", + "required": [ + "username", + "password" + ], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string" + }, + "username": { + "title": "Username", + "type": "string" + }, + "password": { + "title": "Password", + "type": "string" + }, + "scope": { + "title": "Scope", + "type": "string", + "default": "" + }, + "client_id": { + "title": "Client Id", + "type": "string" + }, + "client_secret": { + "title": "Client Secret", + "type": "string" + } + } + }, + "Body_login_api_v1_login_post": { + "title": "Body_login_api_v1_login_post", + "required": [ + "username", + "password" + ], + "type": "object", + "properties": { + "username": { + "title": "Username", + "type": "string" + }, + "password": { + "title": "Password", + "type": "string" + } + } + }, + "Camera": { + "title": "Camera", + "required": [ + "id", + "sn", + "name", + "streamUrl", + "lng", + "lat", + "elevation", + "towards", + "rsuId", + "rsuName", + "countryCode", + "countryName", + "provinceCode", + "provinceName", + "cityCode", + "cityName", + "areaCode", + "areaName", + "desc", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "Camera ID" + }, + "sn": { + "title": "Sn", + "type": "string", + "description": "SN" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Name" + }, + "streamUrl": { + "title": "Streamurl", + "type": "string", + "description": "Stream URL" + }, + "lng": { + "title": "Lng", + "type": "number", + "description": "Lng" + }, + "lat": { + "title": "Lat", + "type": "number", + "description": "Lat" + }, + "elevation": { + "title": "Elevation", + "type": "number", + "description": "Elevation" + }, + "towards": { + "title": "Towards", + "type": "number", + "description": "Towards" + }, + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "countryCode": { + "title": "Countrycode", + "type": "string", + "description": "Country Code" + }, + "countryName": { + "title": "Countryname", + "type": "string", + "description": "Country Name" + }, + "provinceCode": { + "title": "Provincecode", + "type": "string", + "description": "Province Code" + }, + "provinceName": { + "title": "Provincename", + "type": "string", + "description": "Province Name" + }, + "cityCode": { + "title": "Citycode", + "type": "string", + "description": "City Code" + }, + "cityName": { + "title": "Cityname", + "type": "string", + "description": "City Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "areaName": { + "title": "Areaname", + "type": "string", + "description": "Area Name" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "CameraCreate": { + "title": "CameraCreate", + "required": [ + "sn", + "name", + "streamUrl", + "lng", + "lat", + "elevation", + "towards", + "rsuId" + ], + "type": "object", + "properties": { + "sn": { + "title": "Sn", + "type": "string", + "description": "SN" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Name" + }, + "streamUrl": { + "title": "Streamurl", + "type": "string", + "description": "Stream URL" + }, + "lng": { + "title": "Lng", + "type": "number", + "description": "Lng" + }, + "lat": { + "title": "Lat", + "type": "number", + "description": "Lat" + }, + "elevation": { + "title": "Elevation", + "type": "number", + "description": "Elevation" + }, + "towards": { + "title": "Towards", + "type": "number", + "description": "Towards" + }, + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "RSU ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + } + } + }, + "CameraOnlineRateBase": { + "title": "CameraOnlineRateBase", + "required": [ + "online", + "offline", + "notRegister" + ], + "type": "object", + "properties": { + "online": { + "title": "Online", + "type": "integer", + "description": "Online" + }, + "offline": { + "title": "Offline", + "type": "integer", + "description": "Offline" + }, + "notRegister": { + "title": "Notregister", + "type": "integer", + "description": "Not Register" + } + } + }, + "CameraUpdate": { + "title": "CameraUpdate", + "required": [ + "sn", + "name", + "streamUrl", + "lng", + "lat", + "elevation", + "towards", + "rsuId" + ], + "type": "object", + "properties": { + "sn": { + "title": "Sn", + "type": "string", + "description": "SN" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Name" + }, + "streamUrl": { + "title": "Streamurl", + "type": "string", + "description": "Stream URL" + }, + "lng": { + "title": "Lng", + "type": "number", + "description": "Lng" + }, + "lat": { + "title": "Lat", + "type": "number", + "description": "Lat" + }, + "elevation": { + "title": "Elevation", + "type": "number", + "description": "Elevation" + }, + "towards": { + "title": "Towards", + "type": "number", + "description": "Towards" + }, + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "RSU ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + } + } + }, + "Cameras": { + "title": "Cameras", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/Camera" + }, + "description": "Data" + } + } + }, + "City": { + "title": "City", + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "code": { + "title": "Code", + "type": "string", + "description": "City code" + }, + "name": { + "title": "Name", + "type": "string", + "description": "City name" + } + } + }, + "Country": { + "title": "Country", + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "code": { + "title": "Code", + "type": "string", + "description": "Country code" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Country name" + }, + "children": { + "title": "Children", + "type": "array", + "items": {} + } + } + }, + "ErrorMessage": { + "title": "ErrorMessage", + "required": [ + "detail" + ], + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "string", + "description": "Message detail" + } + } + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + }, + "LogLevel": { + "title": "LogLevel", + "enum": [ + "DEBUG", + "INFO", + "WARN", + "ERROR", + "NOLog" + ], + "type": "string", + "description": "An enumeration." + }, + "MAP": { + "title": "MAP", + "type": "object", + "properties": { + "upLimit": { + "title": "Uplimit", + "type": "integer", + "description": "The upper limit of the MAP." + }, + "upFilters": { + "title": "Upfilters", + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": "The upper filters of the MAP." + } + } + }, + "MNG": { + "title": "MNG", + "required": [ + "id", + "rsuName", + "rsuEsn", + "hbRate", + "runningInfoRate", + "addressChg", + "logLevel", + "reboot", + "extendConfig", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "MNG ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "hbRate": { + "title": "Hbrate", + "type": "integer", + "description": "Heartbeat Rate" + }, + "runningInfoRate": { + "title": "Runninginforate", + "type": "integer", + "description": "Running Info Rate" + }, + "addressChg": { + "title": "Addresschg", + "allOf": [ + { + "$ref": "#/components/schemas/AddressChg" + } + ], + "description": "Address Change" + }, + "logLevel": { + "allOf": [ + { + "$ref": "#/components/schemas/LogLevel" + } + ], + "description": "Log Level" + }, + "reboot": { + "allOf": [ + { + "$ref": "#/components/schemas/Reboot" + } + ], + "description": "Reboot" + }, + "extendConfig": { + "title": "Extendconfig", + "type": "string", + "description": "Extend Config" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "MNGCopy": { + "title": "MNGCopy", + "required": [ + "rsus" + ], + "type": "object", + "properties": { + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "type": "integer" + }, + "description": "RSUs" + } + } + }, + "MNGUpdate": { + "title": "MNGUpdate", + "required": [ + "hbRate", + "runningInfoRate", + "addressChg", + "logLevel", + "reboot", + "extendConfig" + ], + "type": "object", + "properties": { + "hbRate": { + "title": "Hbrate", + "type": "integer", + "description": "Heartbeat Rate" + }, + "runningInfoRate": { + "title": "Runninginforate", + "type": "integer", + "description": "Running Info Rate" + }, + "addressChg": { + "title": "Addresschg", + "allOf": [ + { + "$ref": "#/components/schemas/AddressChg" + } + ], + "description": "Address Change" + }, + "logLevel": { + "allOf": [ + { + "$ref": "#/components/schemas/LogLevel" + } + ], + "description": "Log Level" + }, + "reboot": { + "allOf": [ + { + "$ref": "#/components/schemas/Reboot" + } + ], + "description": "Reboot" + }, + "extendConfig": { + "title": "Extendconfig", + "type": "string", + "description": "Extend Config" + } + } + }, + "MNGs": { + "title": "MNGs", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/MNG" + }, + "description": "Data" + } + } + }, + "Map": { + "title": "Map", + "required": [ + "id", + "name", + "address", + "desc", + "amount", + "lat", + "lng", + "createTime", + "countryCode", + "countryName", + "provinceCode", + "provinceName", + "cityCode", + "cityName", + "areaCode", + "areaName" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "Map ID" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Map Name" + }, + "address": { + "title": "Address", + "type": "string", + "description": "Address" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "amount": { + "title": "Amount", + "type": "integer", + "description": "Count of RSUs" + }, + "lat": { + "title": "Lat", + "type": "number", + "description": "Latitude" + }, + "lng": { + "title": "Lng", + "type": "number", + "description": "Longitude" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + }, + "countryCode": { + "title": "Countrycode", + "type": "string", + "description": "Country Code" + }, + "countryName": { + "title": "Countryname", + "type": "string", + "description": "Country Name" + }, + "provinceCode": { + "title": "Provincecode", + "type": "string", + "description": "Province Code" + }, + "provinceName": { + "title": "Provincename", + "type": "string", + "description": "Province Name" + }, + "cityCode": { + "title": "Citycode", + "type": "string", + "description": "City Code" + }, + "cityName": { + "title": "Cityname", + "type": "string", + "description": "City Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "areaName": { + "title": "Areaname", + "type": "string", + "description": "Area Name" + } + } + }, + "MapCreate": { + "title": "MapCreate", + "required": [ + "name", + "areaCode", + "address", + "data" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "Map Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "address": { + "title": "Address", + "type": "string", + "description": "Address" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description", + "default": "" + }, + "data": { + "title": "Data", + "type": "object", + "description": "Data" + } + } + }, + "MapRSU": { + "title": "MapRSU", + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "title": "Data", + "allOf": [ + { + "$ref": "#/components/schemas/MapRSUBase" + } + ], + "description": "Data" + } + } + }, + "MapRSUBase": { + "title": "MapRSUBase", + "required": [ + "mapId", + "rsus" + ], + "type": "object", + "properties": { + "mapId": { + "title": "Mapid", + "type": "integer", + "description": "Map ID" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUsInMapRSU" + }, + "description": "RSUs" + } + } + }, + "MapRSUCreate": { + "title": "MapRSUCreate", + "required": [ + "rsus" + ], + "type": "object", + "properties": { + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "type": "string" + }, + "description": "RSU ESN" + } + } + }, + "MapRSUs": { + "title": "MapRSUs", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/MapRSUsBase" + }, + "description": "Data" + } + } + }, + "MapRSUsBase": { + "title": "MapRSUsBase", + "required": [ + "id", + "rsuName", + "rsuSn", + "onlineStatus", + "rsuStatus", + "deliveryStatus", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "Map RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuSn": { + "title": "Rsusn", + "type": "string", + "description": "RSU SN" + }, + "onlineStatus": { + "title": "Onlinestatus", + "type": "integer", + "description": "Online Status" + }, + "rsuStatus": { + "title": "Rsustatus", + "type": "integer", + "description": "RSU Status" + }, + "deliveryStatus": { + "title": "Deliverystatus", + "type": "integer", + "description": "Delivery Status" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "MapUpdate": { + "title": "MapUpdate", + "required": [ + "name", + "areaCode", + "address" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "address": { + "title": "Address", + "type": "string", + "description": "Address" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "data": { + "title": "Data", + "type": "object", + "description": "Data" + } + } + }, + "Maps": { + "title": "Maps", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/Map" + }, + "description": "Data" + } + } + }, + "Message": { + "title": "Message", + "required": [ + "detail" + ], + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "string", + "description": "Message detail" + } + } + }, + "OnlineRate": { + "title": "OnlineRate", + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "title": "Data", + "allOf": [ + { + "$ref": "#/components/schemas/OnlineRateBase" + } + ], + "description": "Online Rate" + } + } + }, + "OnlineRateBase": { + "title": "OnlineRateBase", + "required": [ + "rsu", + "camera", + "radar" + ], + "type": "object", + "properties": { + "rsu": { + "title": "Rsu", + "allOf": [ + { + "$ref": "#/components/schemas/RSUOnlineRateBase" + } + ], + "description": "RSU Online Rate" + }, + "camera": { + "title": "Camera", + "allOf": [ + { + "$ref": "#/components/schemas/CameraOnlineRateBase" + } + ], + "description": "Camera Online Rate" + }, + "radar": { + "title": "Radar", + "allOf": [ + { + "$ref": "#/components/schemas/RadarOnlineRateBase" + } + ], + "description": "Radar Online Rate" + } + } + }, + "Province": { + "title": "Province", + "required": [ + "code", + "name" + ], + "type": "object", + "properties": { + "code": { + "title": "Code", + "type": "string", + "description": "Province code" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Province name" + } + } + }, + "RSI": { + "title": "RSI", + "type": "object", + "properties": { + "upFilters": { + "title": "Upfilters", + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": "The upper filters of the RSI." + } + } + }, + "RSIEvent": { + "title": "RSIEvent", + "required": [ + "id", + "rsuName", + "rsuEsn", + "address", + "eventClass", + "eventType", + "createTime", + "countryCode", + "countryName", + "provinceCode", + "provinceName", + "cityCode", + "cityName", + "areaCode", + "areaName", + "alertID", + "duration", + "eventStatus", + "timestamp", + "eventSource", + "eventConfidence", + "eventPosition", + "eventRadius", + "eventDescription", + "eventPriority", + "referencePaths" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSI Event ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU esn" + }, + "address": { + "title": "Address", + "type": "string", + "description": "RSU address" + }, + "eventClass": { + "title": "Eventclass", + "type": "string", + "description": "Event class" + }, + "eventType": { + "title": "Eventtype", + "type": "integer", + "description": "Event type" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create time", + "format": "date-time" + }, + "countryCode": { + "title": "Countrycode", + "type": "string", + "description": "Country Code" + }, + "countryName": { + "title": "Countryname", + "type": "string", + "description": "Country Name" + }, + "provinceCode": { + "title": "Provincecode", + "type": "string", + "description": "Province Code" + }, + "provinceName": { + "title": "Provincename", + "type": "string", + "description": "Province Name" + }, + "cityCode": { + "title": "Citycode", + "type": "string", + "description": "City Code" + }, + "cityName": { + "title": "Cityname", + "type": "string", + "description": "City Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "areaName": { + "title": "Areaname", + "type": "string", + "description": "Area Name" + }, + "alertID": { + "title": "Alertid", + "type": "string", + "description": "Alert id" + }, + "duration": { + "title": "Duration", + "type": "integer", + "description": "Duration" + }, + "eventStatus": { + "title": "Eventstatus", + "type": "boolean", + "description": "Event status" + }, + "timestamp": { + "title": "Timestamp", + "type": "string", + "description": "Timestamp" + }, + "eventSource": { + "title": "Eventsource", + "type": "string", + "description": "Event source" + }, + "eventConfidence": { + "title": "Eventconfidence", + "type": "number", + "description": "Event confidence" + }, + "eventPosition": { + "title": "Eventposition", + "type": "object", + "description": "Event position" + }, + "eventRadius": { + "title": "Eventradius", + "type": "number", + "description": "Event radius" + }, + "eventDescription": { + "title": "Eventdescription", + "type": "string", + "description": "Event description" + }, + "eventPriority": { + "title": "Eventpriority", + "type": "integer", + "description": "Event priority" + }, + "referencePaths": { + "title": "Referencepaths", + "type": "string", + "description": "Reference paths" + } + } + }, + "RSIEvents": { + "title": "RSIEvents", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSIEvent" + }, + "description": "Data" + } + } + }, + "RSM": { + "title": "RSM", + "type": "object", + "properties": { + "upLimit": { + "title": "Uplimit", + "type": "integer", + "description": "The upper limit of the RSM." + }, + "upFilters": { + "title": "Upfilters", + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": "The upper filters of the RSM." + } + } + }, + "RSMParticipant": { + "title": "RSMParticipant", + "required": [ + "id", + "ptcId", + "ptcType", + "ptcTypeName", + "source", + "secMark", + "lon", + "lat", + "accuracy", + "speed", + "heading", + "size", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSM Participant ID" + }, + "ptcId": { + "title": "Ptcid", + "type": "integer", + "description": "PTC id" + }, + "ptcType": { + "title": "Ptctype", + "type": "string", + "description": "PTC type" + }, + "ptcTypeName": { + "title": "Ptctypename", + "type": "string", + "description": "PTC type name" + }, + "source": { + "title": "Source", + "type": "integer", + "description": "Source" + }, + "secMark": { + "title": "Secmark", + "type": "integer", + "description": "Sec mark" + }, + "lon": { + "title": "Lon", + "type": "integer", + "description": "Lon" + }, + "lat": { + "title": "Lat", + "type": "integer", + "description": "Lat" + }, + "accuracy": { + "title": "Accuracy", + "type": "string", + "description": "Accuracy" + }, + "speed": { + "title": "Speed", + "type": "integer", + "description": "Speed" + }, + "heading": { + "title": "Heading", + "type": "integer", + "description": "Heading" + }, + "size": { + "title": "Size", + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "Size" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create time", + "format": "date-time" + } + } + }, + "RSMParticipants": { + "title": "RSMParticipants", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSMParticipant" + }, + "description": "Data" + } + } + }, + "RSU": { + "title": "RSU", + "required": [ + "rsuId", + "rsuName", + "rsuEsn", + "rsuIP", + "countryCode", + "countryName", + "provinceCode", + "provinceName", + "cityCode", + "cityName", + "areaCode", + "areaName", + "address", + "rsuStatus", + "onlineStatus", + "createTime", + "updateTime", + "id" + ], + "type": "object", + "properties": { + "rsuId": { + "title": "Rsuid", + "type": "string", + "description": "RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "rsuIP": { + "title": "Rsuip", + "type": "string", + "description": "RSU IP" + }, + "countryCode": { + "title": "Countrycode", + "type": "string", + "description": "Country Code" + }, + "countryName": { + "title": "Countryname", + "type": "string", + "description": "Country Name" + }, + "provinceCode": { + "title": "Provincecode", + "type": "string", + "description": "Province Code" + }, + "provinceName": { + "title": "Provincename", + "type": "string", + "description": "Province Name" + }, + "cityCode": { + "title": "Citycode", + "type": "string", + "description": "City Code" + }, + "cityName": { + "title": "Cityname", + "type": "string", + "description": "City Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "areaName": { + "title": "Areaname", + "type": "string", + "description": "Area Name" + }, + "address": { + "title": "Address", + "type": "string", + "description": "Address" + }, + "rsuStatus": { + "title": "Rsustatus", + "type": "boolean", + "description": "RSU Status" + }, + "onlineStatus": { + "title": "Onlinestatus", + "type": "boolean", + "description": "Online Status" + }, + "rsuModelId": { + "title": "Rsumodelid", + "type": "integer", + "description": "RSU Model ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + }, + "version": { + "title": "Version", + "type": "string", + "description": "Version" + }, + "updateTime": { + "title": "Updatetime", + "type": "string", + "description": "Update Time", + "format": "date-time" + }, + "location": { + "title": "Location", + "allOf": [ + { + "$ref": "#/components/schemas/RSULocation" + } + ], + "description": "Location" + }, + "id": { + "title": "Id", + "type": "integer", + "description": "RSU ID" + }, + "config": { + "title": "Config", + "type": "object", + "description": "Config" + } + } + }, + "RSUConfig": { + "title": "RSUConfig", + "required": [ + "id", + "name", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSU Config ID" + }, + "name": { + "title": "Name", + "type": "string", + "description": "RSU Config name" + }, + "bsmConfig": { + "title": "Bsmconfig", + "allOf": [ + { + "$ref": "#/components/schemas/BSM" + } + ], + "description": "BSM" + }, + "rsiConfig": { + "title": "Rsiconfig", + "allOf": [ + { + "$ref": "#/components/schemas/RSI" + } + ], + "description": "RSI" + }, + "rsmConfig": { + "title": "Rsmconfig", + "allOf": [ + { + "$ref": "#/components/schemas/RSM" + } + ], + "description": "RSM" + }, + "mapConfig": { + "title": "Mapconfig", + "allOf": [ + { + "$ref": "#/components/schemas/MAP" + } + ], + "description": "MAP" + }, + "spatConfig": { + "title": "Spatconfig", + "allOf": [ + { + "$ref": "#/components/schemas/SPAT" + } + ], + "description": "SPAT" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create time", + "format": "date-time" + } + } + }, + "RSUConfigCreate": { + "title": "RSUConfigCreate", + "required": [ + "name", + "bsm", + "rsi", + "rsm", + "map" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "RSU Config name" + }, + "bsm": { + "title": "Bsm", + "allOf": [ + { + "$ref": "#/components/schemas/BSM" + } + ], + "description": "BSM" + }, + "rsi": { + "title": "Rsi", + "allOf": [ + { + "$ref": "#/components/schemas/RSI" + } + ], + "description": "RSI" + }, + "rsm": { + "title": "Rsm", + "allOf": [ + { + "$ref": "#/components/schemas/RSM" + } + ], + "description": "RSM" + }, + "map": { + "title": "Map", + "allOf": [ + { + "$ref": "#/components/schemas/MAP" + } + ], + "description": "MAP" + }, + "spat": { + "title": "Spat", + "allOf": [ + { + "$ref": "#/components/schemas/SPAT" + } + ], + "description": "SPAT" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "type": "integer" + }, + "description": "RSUs" + } + } + }, + "RSUConfigRSUInRSU": { + "title": "RSUConfigRSUInRSU", + "required": [ + "id", + "status", + "rsu_id", + "rsu_config_id", + "create_time" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "ID" + }, + "status": { + "title": "Status", + "type": "boolean", + "description": "Status" + }, + "rsu_id": { + "title": "Rsu Id", + "type": "integer", + "description": "RSU ID" + }, + "rsu_config_id": { + "title": "Rsu Config Id", + "type": "integer", + "description": "RSU Config ID" + }, + "create_time": { + "title": "Create Time", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "RSUConfigUpdate": { + "title": "RSUConfigUpdate", + "required": [ + "name", + "bsm", + "rsi", + "rsm", + "map" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "RSU Config name" + }, + "bsm": { + "title": "Bsm", + "allOf": [ + { + "$ref": "#/components/schemas/BSM" + } + ], + "description": "BSM" + }, + "rsi": { + "title": "Rsi", + "allOf": [ + { + "$ref": "#/components/schemas/RSI" + } + ], + "description": "RSI" + }, + "rsm": { + "title": "Rsm", + "allOf": [ + { + "$ref": "#/components/schemas/RSM" + } + ], + "description": "RSM" + }, + "map": { + "title": "Map", + "allOf": [ + { + "$ref": "#/components/schemas/MAP" + } + ], + "description": "MAP" + }, + "spat": { + "title": "Spat", + "allOf": [ + { + "$ref": "#/components/schemas/SPAT" + } + ], + "description": "SPAT" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "type": "integer" + }, + "description": "RSUs" + } + } + }, + "RSUConfigWithRSUs": { + "title": "RSUConfigWithRSUs", + "required": [ + "id", + "name", + "createTime", + "rsus" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSU Config ID" + }, + "name": { + "title": "Name", + "type": "string", + "description": "RSU Config name" + }, + "bsmConfig": { + "title": "Bsmconfig", + "allOf": [ + { + "$ref": "#/components/schemas/BSM" + } + ], + "description": "BSM" + }, + "rsiConfig": { + "title": "Rsiconfig", + "allOf": [ + { + "$ref": "#/components/schemas/RSI" + } + ], + "description": "RSI" + }, + "rsmConfig": { + "title": "Rsmconfig", + "allOf": [ + { + "$ref": "#/components/schemas/RSM" + } + ], + "description": "RSM" + }, + "mapConfig": { + "title": "Mapconfig", + "allOf": [ + { + "$ref": "#/components/schemas/MAP" + } + ], + "description": "MAP" + }, + "spatConfig": { + "title": "Spatconfig", + "allOf": [ + { + "$ref": "#/components/schemas/SPAT" + } + ], + "description": "SPAT" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create time", + "format": "date-time" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSU" + }, + "description": "RSUs" + } + } + }, + "RSUConfigs": { + "title": "RSUConfigs", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUConfig" + }, + "description": "Data" + } + } + }, + "RSUCreate": { + "title": "RSUCreate", + "required": [ + "rsuId", + "rsuName", + "rsuEsn", + "rsuIP", + "areaCode", + "address" + ], + "type": "object", + "properties": { + "tmpId": { + "title": "Tmpid", + "type": "integer", + "description": "Temporary RSU ID", + "default": 0 + }, + "rsuId": { + "title": "Rsuid", + "type": "string", + "description": "RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "rsuIP": { + "title": "Rsuip", + "type": "string", + "description": "RSU IP" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "address": { + "title": "Address", + "type": "string", + "description": "Address" + }, + "rsuModelId": { + "title": "Rsumodelid", + "type": "integer", + "description": "RSU Model ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + } + } + }, + "RSUDetail": { + "title": "RSUDetail", + "required": [ + "rsuId", + "rsuName", + "rsuEsn", + "rsuIP", + "countryCode", + "countryName", + "provinceCode", + "provinceName", + "cityCode", + "cityName", + "areaCode", + "areaName", + "address", + "rsuStatus", + "onlineStatus", + "createTime", + "updateTime", + "id", + "config" + ], + "type": "object", + "properties": { + "rsuId": { + "title": "Rsuid", + "type": "string", + "description": "RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "rsuIP": { + "title": "Rsuip", + "type": "string", + "description": "RSU IP" + }, + "countryCode": { + "title": "Countrycode", + "type": "string", + "description": "Country Code" + }, + "countryName": { + "title": "Countryname", + "type": "string", + "description": "Country Name" + }, + "provinceCode": { + "title": "Provincecode", + "type": "string", + "description": "Province Code" + }, + "provinceName": { + "title": "Provincename", + "type": "string", + "description": "Province Name" + }, + "cityCode": { + "title": "Citycode", + "type": "string", + "description": "City Code" + }, + "cityName": { + "title": "Cityname", + "type": "string", + "description": "City Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "areaName": { + "title": "Areaname", + "type": "string", + "description": "Area Name" + }, + "address": { + "title": "Address", + "type": "string", + "description": "Address" + }, + "rsuStatus": { + "title": "Rsustatus", + "type": "boolean", + "description": "RSU Status" + }, + "onlineStatus": { + "title": "Onlinestatus", + "type": "boolean", + "description": "Online Status" + }, + "rsuModelId": { + "title": "Rsumodelid", + "type": "integer", + "description": "RSU Model ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + }, + "version": { + "title": "Version", + "type": "string", + "description": "Version" + }, + "updateTime": { + "title": "Updatetime", + "type": "string", + "description": "Update Time", + "format": "date-time" + }, + "location": { + "title": "Location", + "allOf": [ + { + "$ref": "#/components/schemas/RSULocation" + } + ], + "description": "Location" + }, + "id": { + "title": "Id", + "type": "integer", + "description": "RSU ID" + }, + "config": { + "title": "Config", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUConfigRSUInRSU" + }, + "description": "RSU Config RSU" + } + } + }, + "RSUInRSULog": { + "title": "RSUInRSULog", + "required": [ + "id", + "rsuName", + "rsuEsn" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + } + } + }, + "RSUInRSUQuery": { + "title": "RSUInRSUQuery", + "required": [ + "rsuId", + "rsuEsn", + "rsuName" + ], + "type": "object", + "properties": { + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "The ID of the RSU" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU name" + } + } + }, + "RSULocation": { + "title": "RSULocation", + "type": "object", + "properties": { + "lat": { + "title": "Lat", + "type": "number", + "description": "Latitude" + }, + "lon": { + "title": "Lon", + "type": "number", + "description": "Longitude" + } + } + }, + "RSULog": { + "title": "RSULog", + "required": [ + "id", + "uploadUrl", + "userId", + "password", + "transprotocal", + "createTime", + "rsus" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSU Log ID" + }, + "uploadUrl": { + "title": "Uploadurl", + "type": "string", + "description": "Upload URL" + }, + "userId": { + "title": "Userid", + "type": "string", + "description": "User ID" + }, + "password": { + "title": "Password", + "type": "string", + "description": "Password" + }, + "transprotocal": { + "allOf": [ + { + "$ref": "#/components/schemas/TransProtocal" + } + ], + "description": "Transprotocal" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUInRSULog" + }, + "description": "RSUs" + } + } + }, + "RSULogCreate": { + "title": "RSULogCreate", + "required": [ + "uploadUrl", + "userId", + "password", + "transprotocal", + "rsus" + ], + "type": "object", + "properties": { + "uploadUrl": { + "title": "Uploadurl", + "type": "string", + "description": "Upload URL" + }, + "userId": { + "title": "Userid", + "type": "string", + "description": "User ID" + }, + "password": { + "title": "Password", + "type": "string", + "description": "Password" + }, + "transprotocal": { + "allOf": [ + { + "$ref": "#/components/schemas/TransProtocal" + } + ], + "description": "Transprotocal" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "type": "integer" + }, + "description": "RSUs" + } + } + }, + "RSULogUpdate": { + "title": "RSULogUpdate", + "required": [ + "uploadUrl", + "userId", + "password", + "transprotocal", + "rsus" + ], + "type": "object", + "properties": { + "uploadUrl": { + "title": "Uploadurl", + "type": "string", + "description": "Upload URL" + }, + "userId": { + "title": "Userid", + "type": "string", + "description": "User ID" + }, + "password": { + "title": "Password", + "type": "string", + "description": "Password" + }, + "transprotocal": { + "allOf": [ + { + "$ref": "#/components/schemas/TransProtocal" + } + ], + "description": "Transprotocal" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "type": "integer" + }, + "description": "RSUs" + } + } + }, + "RSULogs": { + "title": "RSULogs", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSULog" + }, + "description": "Data" + } + } + }, + "RSUModel": { + "title": "RSUModel", + "required": [ + "id", + "name", + "manufacturer", + "desc", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSU Model ID" + }, + "name": { + "title": "Name", + "type": "string", + "description": "RSU Model Name" + }, + "manufacturer": { + "title": "Manufacturer", + "type": "string", + "description": "Manufacturer" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "RSUModelCreate": { + "title": "RSUModelCreate", + "required": [ + "name", + "manufacturer" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "RSU Model Name" + }, + "manufacturer": { + "title": "Manufacturer", + "type": "string", + "description": "Manufacturer" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description", + "default": "" + } + } + }, + "RSUModelUpdate": { + "title": "RSUModelUpdate", + "required": [ + "name", + "manufacturer" + ], + "type": "object", + "properties": { + "name": { + "title": "Name", + "type": "string", + "description": "RSU Model Name" + }, + "manufacturer": { + "title": "Manufacturer", + "type": "string", + "description": "Manufacturer" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description", + "default": "" + } + } + }, + "RSUModels": { + "title": "RSUModels", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUModel" + }, + "description": "Data" + } + } + }, + "RSUOnlineRateBase": { + "title": "RSUOnlineRateBase", + "required": [ + "online", + "offline", + "notRegister" + ], + "type": "object", + "properties": { + "online": { + "title": "Online", + "type": "integer", + "description": "Online" + }, + "offline": { + "title": "Offline", + "type": "integer", + "description": "Offline" + }, + "notRegister": { + "title": "Notregister", + "type": "integer", + "description": "Not Register" + } + } + }, + "RSUQueries": { + "title": "RSUQueries", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUQuery" + }, + "description": "Data" + } + } + }, + "RSUQuery": { + "title": "RSUQuery", + "required": [ + "id", + "queryType", + "timeType", + "createTime", + "rsus" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSU Query ID" + }, + "queryType": { + "title": "Querytype", + "type": "integer", + "description": "Query Type" + }, + "timeType": { + "title": "Timetype", + "type": "integer", + "description": "Time Type" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUInRSUQuery" + }, + "description": "RSUs" + } + } + }, + "RSUQueryCreate": { + "title": "RSUQueryCreate", + "required": [ + "queryType", + "timeType", + "rsus" + ], + "type": "object", + "properties": { + "queryType": { + "title": "Querytype", + "type": "integer", + "description": "Query Type" + }, + "timeType": { + "title": "Timetype", + "type": "integer", + "description": "Time Type" + }, + "rsus": { + "title": "Rsus", + "type": "array", + "items": { + "type": "integer" + }, + "description": "RSUs" + } + } + }, + "RSUQueryDetail": { + "title": "RSUQueryDetail", + "type": "object", + "properties": { + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUQueryDetailBase" + }, + "description": "RSU Query Detail" + } + } + }, + "RSUQueryDetailBase": { + "title": "RSUQueryDetailBase", + "required": [ + "rsuName", + "rsuEsn", + "queryType", + "timeType", + "createTime", + "data" + ], + "type": "object", + "properties": { + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "The ID of the RSU" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "queryType": { + "title": "Querytype", + "type": "integer", + "description": "Query Type" + }, + "timeType": { + "title": "Timetype", + "type": "integer", + "description": "Time Type" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "type": "object" + }, + "description": "Data" + } + } + }, + "RSUTMP": { + "title": "RSUTMP", + "required": [ + "id", + "rsuId", + "rsuName", + "rsuEsn", + "version", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "RSU TMP ID" + }, + "rsuId": { + "title": "Rsuid", + "type": "string", + "description": "RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "version": { + "title": "Version", + "type": "string", + "description": "Version" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "RSUTMPs": { + "title": "RSUTMPs", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSUTMP" + }, + "description": "Data" + } + } + }, + "RSUUpdate": { + "title": "RSUUpdate", + "type": "object", + "properties": { + "rsuId": { + "title": "Rsuid", + "type": "string", + "description": "RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "rsuIP": { + "title": "Rsuip", + "type": "string", + "description": "RSU IP" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "address": { + "title": "Address", + "type": "string", + "description": "Address" + }, + "rsuModelId": { + "title": "Rsumodelid", + "type": "integer", + "description": "RSU Model ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "rsuStatus": { + "title": "Rsustatus", + "type": "boolean", + "description": "RSU Status" + } + } + }, + "RSUs": { + "title": "RSUs", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/RSU" + }, + "description": "Data" + } + } + }, + "RSUsInMapRSU": { + "title": "RSUsInMapRSU", + "required": [ + "id", + "rsuId", + "status", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "ID" + }, + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "RSU ID" + }, + "status": { + "title": "Status", + "type": "integer", + "description": "Status" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "Radar": { + "title": "Radar", + "required": [ + "id", + "sn", + "name", + "radarIP", + "lng", + "lat", + "elevation", + "towards", + "rsuId", + "rsuName", + "countryCode", + "countryName", + "provinceCode", + "provinceName", + "cityCode", + "cityName", + "areaCode", + "areaName", + "desc", + "createTime" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "integer", + "description": "Radar ID" + }, + "sn": { + "title": "Sn", + "type": "string", + "description": "Radar SN" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Radar Name" + }, + "radarIP": { + "title": "Radarip", + "type": "string", + "description": "Radar IP" + }, + "lng": { + "title": "Lng", + "type": "string", + "description": "Longitude" + }, + "lat": { + "title": "Lat", + "type": "string", + "description": "Latitude" + }, + "elevation": { + "title": "Elevation", + "type": "string", + "description": "Elevation" + }, + "towards": { + "title": "Towards", + "type": "string", + "description": "Towards" + }, + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "RSU ID" + }, + "rsuName": { + "title": "Rsuname", + "type": "string", + "description": "RSU Name" + }, + "countryCode": { + "title": "Countrycode", + "type": "string", + "description": "Country Code" + }, + "countryName": { + "title": "Countryname", + "type": "string", + "description": "Country Name" + }, + "provinceCode": { + "title": "Provincecode", + "type": "string", + "description": "Province Code" + }, + "provinceName": { + "title": "Provincename", + "type": "string", + "description": "Province Name" + }, + "cityCode": { + "title": "Citycode", + "type": "string", + "description": "City Code" + }, + "cityName": { + "title": "Cityname", + "type": "string", + "description": "City Name" + }, + "areaCode": { + "title": "Areacode", + "type": "string", + "description": "Area Code" + }, + "areaName": { + "title": "Areaname", + "type": "string", + "description": "Area Name" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description" + }, + "createTime": { + "title": "Createtime", + "type": "string", + "description": "Create Time", + "format": "date-time" + } + } + }, + "RadarCreate": { + "title": "RadarCreate", + "required": [ + "sn", + "name", + "lng", + "lat", + "elevation", + "towards" + ], + "type": "object", + "properties": { + "sn": { + "title": "Sn", + "type": "string", + "description": "Radar SN" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Radar Name" + }, + "radarIp": { + "title": "Radarip", + "type": "string", + "description": "Radar IP" + }, + "lng": { + "title": "Lng", + "type": "string", + "description": "Longitude" + }, + "lat": { + "title": "Lat", + "type": "string", + "description": "Latitude" + }, + "elevation": { + "title": "Elevation", + "type": "string", + "description": "Elevation" + }, + "towards": { + "title": "Towards", + "type": "string", + "description": "Towards" + }, + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "RSU ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description", + "default": "" + } + } + }, + "RadarOnlineRateBase": { + "title": "RadarOnlineRateBase", + "required": [ + "online", + "offline", + "notRegister" + ], + "type": "object", + "properties": { + "online": { + "title": "Online", + "type": "integer", + "description": "Online" + }, + "offline": { + "title": "Offline", + "type": "integer", + "description": "Offline" + }, + "notRegister": { + "title": "Notregister", + "type": "integer", + "description": "Not Register" + } + } + }, + "RadarUpdate": { + "title": "RadarUpdate", + "required": [ + "sn", + "name", + "lng", + "lat", + "elevation", + "towards" + ], + "type": "object", + "properties": { + "sn": { + "title": "Sn", + "type": "string", + "description": "Radar SN" + }, + "name": { + "title": "Name", + "type": "string", + "description": "Radar Name" + }, + "radarIp": { + "title": "Radarip", + "type": "string", + "description": "Radar IP" + }, + "lng": { + "title": "Lng", + "type": "string", + "description": "Longitude" + }, + "lat": { + "title": "Lat", + "type": "string", + "description": "Latitude" + }, + "elevation": { + "title": "Elevation", + "type": "string", + "description": "Elevation" + }, + "towards": { + "title": "Towards", + "type": "string", + "description": "Towards" + }, + "rsuId": { + "title": "Rsuid", + "type": "integer", + "description": "RSU ID" + }, + "desc": { + "title": "Desc", + "type": "string", + "description": "Description", + "default": "" + } + } + }, + "Radars": { + "title": "Radars", + "required": [ + "total", + "data" + ], + "type": "object", + "properties": { + "total": { + "title": "Total", + "type": "integer", + "description": "Total" + }, + "data": { + "title": "Data", + "type": "array", + "items": { + "$ref": "#/components/schemas/Radar" + }, + "description": "Data" + } + } + }, + "Reboot": { + "title": "Reboot", + "enum": [ + "not_reboot", + "reboot" + ], + "type": "string", + "description": "An enumeration." + }, + "RouteInfo": { + "title": "RouteInfo", + "required": [ + "vehicleTotal", + "averageSpeed", + "pedestrianTotal", + "congestion" + ], + "type": "object", + "properties": { + "vehicleTotal": { + "title": "Vehicletotal", + "type": "integer", + "description": "Vehicle Total" + }, + "averageSpeed": { + "title": "Averagespeed", + "type": "integer", + "description": "Average Speed" + }, + "pedestrianTotal": { + "title": "Pedestriantotal", + "type": "integer", + "description": "Pedestrian Total" + }, + "congestion": { + "title": "Congestion", + "type": "string", + "description": "Congestion" + } + } + }, + "RouteInfoCreate": { + "title": "RouteInfoCreate", + "required": [ + "rsuEsn" + ], + "type": "object", + "properties": { + "rsuEsn": { + "title": "Rsuesn", + "type": "string", + "description": "RSU ESN" + }, + "vehicleTotal": { + "title": "Vehicletotal", + "type": "integer", + "description": "Vehicle Total", + "default": 0 + }, + "averageSpeed": { + "title": "Averagespeed", + "type": "integer", + "description": "Average Speed", + "default": 0 + }, + "pedestrianTotal": { + "title": "Pedestriantotal", + "type": "integer", + "description": "Pedestrian Total", + "default": 0 + }, + "congestion": { + "title": "Congestion", + "type": "string", + "description": "Congestion", + "default": "Unknown" + } + } + }, + "SPAT": { + "title": "SPAT", + "type": "object", + "properties": { + "upLimit": { + "title": "Uplimit", + "type": "integer", + "description": "The upper limit of the SPAT." + }, + "upFilters": { + "title": "Upfilters", + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": "The upper filters of the SPAT." + } + } + }, + "SampleMode": { + "title": "SampleMode", + "enum": [ + "ByAll", + "ByID" + ], + "type": "string", + "description": "An enumeration." + }, + "Token": { + "title": "Token", + "required": [ + "access_token", + "token_type" + ], + "type": "object", + "properties": { + "access_token": { + "title": "Access Token", + "type": "string", + "description": "Access token" + }, + "token_type": { + "title": "Token Type", + "type": "string", + "description": "Token type" + } + } + }, + "TransProtocal": { + "title": "TransProtocal", + "enum": [ + "http", + "https", + "ftp", + "sftp", + "other" + ], + "type": "string", + "description": "An enumeration." + }, + "User": { + "title": "User", + "type": "object", + "properties": { + "username": { + "title": "Username", + "type": "string", + "description": "Username" + }, + "is_active": { + "title": "Is Active", + "type": "boolean", + "description": "Is active", + "default": true + }, + "id": { + "title": "Id", + "type": "integer", + "description": "User ID" + } + } + }, + "UserCreate": { + "title": "UserCreate", + "required": [ + "username", + "password" + ], + "type": "object", + "properties": { + "username": { + "title": "Username", + "type": "string", + "description": "Username" + }, + "is_active": { + "title": "Is Active", + "type": "boolean", + "description": "Is active", + "default": true + }, + "password": { + "title": "Password", + "type": "string", + "description": "Password" + } + } + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + } + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + } + } + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": {}, + "tokenUrl": "/api/v1/login/access-token" + } + } + } + } + } +} \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..6fb0c83 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,10 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +flake8>=4.0.1 # MIT +isort>=5.10.1 # MIT +black>=22.3.0 # MIT +mypy>=0.961 # MIT +lxml>=4.9.0 # BSD +pytest>=7.1.2 # MIT diff --git a/tools/datainit.py b/tools/datainit.py new file mode 100644 index 0000000..3bbe4de --- /dev/null +++ b/tools/datainit.py @@ -0,0 +1,135 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from oslo_config import cfg +from sqlalchemy.orm import Session + +from dandelion import conf, constants, version +from dandelion.core.security import get_password_hash +from dandelion.db import session as db_session +from dandelion.models import MNG, RSU, Area, City, Country, Province, RSUConfig, RSUConfigRSU, User +from dandelion.models.rsu_model import RSUModel + +CONF: cfg = conf.CONF + + +def init_db() -> None: + CONF( + args=["--config-file", constants.CONFIG_FILE_PATH], + project=constants.PROJECT_NAME, + version=version.version_string(), + ) + + db_session.setup_db() + session_: Session = db_session.DB_SESSION_LOCAL() + + user = User() + user.username = "admin" + user.hashed_password = get_password_hash("dandelion") + user.is_active = 1 + user.is_superuser = 1 + session_.add(user) + + country1 = Country() + country1.code = "CN" + country1.name = "中国" + session_.add(country1) + + province2 = Province() + province2.country_code = "CN" + province2.code = "320000" + province2.name = "江苏省" + session_.add(province2) + + city1 = City() + city1.province_code = "320000" + city1.code = "320100" + city1.name = "南京市" + session_.add(city1) + + city2 = City() + city2.province_code = "320000" + city2.code = "320200" + city2.name = "无锡市" + session_.add(city2) + + area1 = Area() + area1.city_code = "320100" + area1.code = "320115" + area1.name = "江宁区" + session_.add(area1) + + area2 = Area() + area2.city_code = "320100" + area2.code = "320106" + area2.name = "鼓楼区" + session_.add(area2) + + area3 = Area() + area3.city_code = "320200" + area3.code = "320211" + area3.name = "滨湖区" + session_.add(area3) + + rsu_model1 = RSUModel() + rsu_model1.name = "RSU1" + rsu_model1.manufacturer = "华为" + rsu_model1.desc = "RSU1的描述" + session_.add(rsu_model1) + session_.commit() + session_.refresh(rsu_model1) + + rsu1 = RSU() + rsu1.rsu_id = "45348" + rsu1.rsu_esn = "R328328" + rsu1.rsu_name = "RSU01" + rsu1.rsu_ip = "192.168.0.102" + rsu1.version = "v1" + rsu1.rsu_status = True + rsu1.online_status = False + rsu1.location = {} + rsu1.config = {} + rsu1.rsu_model_id = rsu_model1.id + rsu1.area_code = "320115" + rsu1.address = "江宁交叉路口" + rsu1.desc = "" + + mng = MNG() + mng.heartbeat_rate = 0 + mng.running_info_rate = 0 + mng.log_level = "NOLog" + mng.reboot = "not_reboot" + mng.address_change = dict(cssUrl="", time=0) + mng.extend_config = "" + rsu1.mng = mng + session_.add(rsu1) + + config1 = RSUConfig() + config1.name = "测试01" + config1.bsm = {} + config1.rsi = {} + config1.rsm = {} + config1.map = {} + config1.spat = {} + session_.add(config1) + + config_rsu1 = RSUConfigRSU() + config_rsu1.rsu = rsu1 + config_rsu1.rsu_config = config1 + session_.add(config_rsu1) + + session_.commit() + + +init_db() diff --git a/tools/generate_swagger.py b/tools/generate_swagger.py new file mode 100644 index 0000000..04885c8 --- /dev/null +++ b/tools/generate_swagger.py @@ -0,0 +1,51 @@ +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import json +import sys + +import click + +from dandelion.main import app + + +class CommandException(Exception): + EXIT_CODE = 1 + + +@click.command(help="Generate swagger file.") +@click.option( + "-o", + "--output-file", + "output_file_path", + default="swagger.json", + help=( + "The path of the output file, this file is used to generate a OpenAPI file for " + "use in the development process. (Default value: swagger.json)" + ), +) +def main(output_file_path: str) -> None: + try: + swagger_dict = app.openapi() + with open(output_file_path, mode="w") as f: + f.write(json.dumps(swagger_dict, indent=4)) + + except CommandException as e: + sys.exit(e.EXIT_CODE) + + +if __name__ == "__main__": + main() diff --git a/tools/run_service.sh b/tools/run_service.sh new file mode 100755 index 0000000..2a8648c --- /dev/null +++ b/tools/run_service.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# +# Copyright 2022 99Cloud, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex + +echo "/usr/local/bin/gunicorn -c /etc/dandelion/gunicorn.py dandelion.main:app" >/run_command + +mapfile -t CMD < <(tail /run_command | xargs -n 1) + +if [[ "${!KOLLA_BOOTSTRAP[*]}" ]]; then + cd /dandelion/ + alembic upgrade head + python /dandelion/tools/datainit.py + exit 0 +fi + +echo "Running command: ${CMD[*]}" +exec "${CMD[@]}" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1ab37ad --- /dev/null +++ b/tox.ini @@ -0,0 +1,93 @@ +[tox] +minversion = 3.18.0 +requires = virtualenv>=20.4.2 +skipsdist = True +envlist = pep8 +# this allows tox to infer the base python from the environment name +# and override any basepython configured in this file +ignore_basepython_conflict=true + +[testenv] +basepython = python3 +setenv = VIRTUAL_ENV={envdir} + PYTHONWARNINGS=default::DeprecationWarning + OS_STDOUT_CAPTURE=1 + OS_STDERR_CAPTURE=1 +usedevelop = True + +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +allowlist_externals = + find + bash + isort +passenv = *_proxy *_PROXY + +[testenv:venv] +deps = + {[testenv]deps} +extras = +commands = + {posargs} + +[testenv:install] +deps = + {[testenv]deps} +extras = +commands = + {posargs} + +[testenv:mypy] +description = + Run type checks. +envdir = {toxworkdir}/shared +extras = +commands = + mypy dandelion + +[testenv:pep8] +description = + Run style and type checks. +envdir = {toxworkdir}/shared +deps = + {[testenv]deps} +extras = +commands = + {[testenv:mypy]commands} + isort --check-only --diff . + black --check --diff --color --line-length 99 . + flake8 . + +[testenv:pep8-format] +description = + Run code format. +envdir = {toxworkdir}/shared +deps = + {[testenv]deps} +extras = +commands = + isort . + black --line-length 99 . + +[testenv:genswagger] +envdir = {toxworkdir}/shared +description = + Generate swagger files. +commands = + python tools/generate_swagger.py -o swagger.json + +[testenv:genconfig] +envdir = {toxworkdir}/shared +extras = +commands = + oslo-config-generator --config-file=etc/dandelion/dandelion-config-generator.conf + +[flake8] +# E203 whitespace before ':' +extend-ignore = E203 +max-line-length = 99 +max-doc-length = 99 +show-source = True +exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,releasenotes