diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9e259dd..edd76e2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,4 @@ blank_issues_enabled: true contact_links: - name: Questions about: Please use one of the forums for questions or general discussions - url: https://github.com/linktimecloud/template-project/pulls \ No newline at end of file + url: https://github.com/linktimecloud/kdp-catalog-manager/issues \ No newline at end of file diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 530a4e3..294ae91 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -4,43 +4,45 @@ name: CI-Build on: - push: - branches: - - main - - release-* - pull_request: - branches: - - main - - release-* + release: + types: + - published jobs: build-docker-images: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Get the version - id: get_version - run: | - VERSION=${GITHUB_REF#refs/tags/} - echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT - - - name: Get image registry - id: get_image_registry + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: 'pip' # caching pip dependencies + - run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov + if [ -f docker/python/requirements.txt ]; then pip install -r docker/python/requirements.txt; fi + + - name: Docker Login + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + username: ${{ secrets.REG_USER }} + password: ${{ secrets.REG_PASSWD }} + + - name: Get Version + id: pversion run: | - echo "IMG_REGISTRY=${{ secrets.NX_ALIYUN_REGISTRY }}" >> $GITHUB_OUTPUT - -# - name: Docker Login -# uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 -# with: -# registry: ${{ secrets.NX_ALIYUN_REGISTRY }} -# username: ${{ secrets.NX_ALIYUN_USERNAME }} -# password: ${{ secrets.NX_ALIYUN_PASSWORD }} - -# - name: Build Images -# run: make docker-build-apiserver IMG_REGISTRY=${{ steps.get_image_registry.outputs.IMG_REGISTRY }} VERSION=${{ steps.get_version.outputs.VERSION }} -# -# - name: Push Images -# run: make docker-push-apiserver IMG_REGISTRY=${{ steps.get_image_registry.outputs.IMG_REGISTRY }} VERSION=${{ steps.get_version.outputs.VERSION }} - - + if [[ "${{ github.ref_name }}" =~ ^[.\|v][0-9]{1,}.[0-9]{1,}[.\|-][0-9]{1,} ]];then + VERSION=${{ github.ref_name }} + else + VERSION="latest" + fi + echo "git_revision=$VERSION" >> $GITHUB_OUTPUT + + - name: Build Images + id: build + run: make docker-build IMG_REGISTRY=${{ secrets.CONTAINER_REGISTRY }} VERSION=${{ steps.pversion.outputs.git_revision }} + + - name: Push Images + run: make docker-push IMG_REGISTRY=${{ secrets.CONTAINER_REGISTRY }} VERSION=${{ steps.pversion.outputs.git_revision }} diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 7c1a7c5..174573b 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -3,8 +3,7 @@ name: Unit-Test on: push: branches: - - main - - release-* + - "*" workflow_dispatch: {} pull_request: branches: @@ -14,15 +13,11 @@ on: permissions: contents: read -env: - # Common versions - GO_VERSION: "1.21" - jobs: detect-noop: permissions: actions: write # for fkirc/skip-duplicate-actions to skip or stop workflow runs - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest outputs: noop: ${{ steps.noop.outputs.should_skip }} steps: @@ -36,27 +31,22 @@ jobs: continue-on-error: true unit-tests: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest needs: detect-noop if: needs.detect-noop.outputs.noop != 'true' - steps: - - name: Set up Go - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 with: - go-version: ${{ env.GO_VERSION }} - - - name: Check out code into the Go module directory - uses: actions/checkout@cd7d8d697e10461458bc61a30d094dc601a8b017 - with: - submodules: true - - - name: Cache Go Dependencies - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 - with: - path: .work/pkg - key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} - restore-keys: ${{ runner.os }}-pkg- - - # - name: Run Make test - # run: make test + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pytest-cov + if [ -f docker/python/requirements.txt ]; then pip install -r docker/python/requirements.txt; fi + + - name: Test with pytest + run: | + export Test=true + pytest --cov --cov-report term --cov-report xml:coverage.xml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7aa4648 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +FROM python:3.10.13-bullseye AS builder + +WORKDIR /workspace + +ENV VIRTUAL_ENV=/opt/venv +RUN python3 -m venv ${VIRTUAL_ENV} +ENV PATH="${VIRTUAL_ENV}/bin:$PATH" + +# Install Python dependencies +COPY docker/python/requirements.txt . +COPY docker/bin/entrypoint.sh . +#COPY version.py . +COPY kdp_catalog_manager kdp_catalog_manager/ +RUN ${VIRTUAL_ENV}/bin/pip3 install --no-cache-dir --upgrade pip setuptools Cython==3.0.8 \ + && ${VIRTUAL_ENV}/bin/pip3 install --no-cache-dir --upgrade --force-reinstall -r requirements.txt \ + && rm -f requirements.txt \ + && cd kdp_catalog_manager && python setup.py build_ext --inplace \ + && ${VIRTUAL_ENV}/bin/python setup.py build_ext clean -a && rm -f setup.py && rm -rf build + + +FROM python:3.10.13-bullseye + +ENV TZ=${TZ:-Asia/Shanghai} +ARG RUNTIME_HOME +ENV RUNTIME_HOME=${RUNTIME_HOME:-/opt/bdos/kdp/bdos-core} +ENV BDOS_USER=${BDOS_USER:-bdos} +ENV BDOS_USER_HOME=${BDOS_USER_HOME:-/home/${BDOS_USER}} + + +WORKDIR ${RUNTIME_HOME} + +RUN apt-get update \ + && apt-get -y install sudo \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && cp /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo ${TZ} > /etc/timezone \ + && adduser -q --disabled-password --shell /bin/bash ${BDOS_USER} \ + && echo "${BDOS_USER} ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/${BDOS_USER} \ + && chmod 0440 /etc/sudoers.d/${BDOS_USER} \ + && chown -R ${BDOS_USER}:${BDOS_USER} ${BDOS_USER_HOME} \ + && mkdir ${RUNTIME_HOME}/logs \ + && chown -R ${BDOS_USER}:${BDOS_USER} ${RUNTIME_HOME} + +ENV PYTHONPATH=${RUNTIME_HOME} +ENV VIRTUAL_ENV=/opt/venv +ENV PATH="${VIRTUAL_ENV}/bin:$PATH" +COPY --chown=${BDOS_USER}:${BDOS_USER} --from=builder /workspace . +COPY --chown=${BDOS_USER}:${BDOS_USER} --from=builder /opt/venv /opt/venv + +USER ${BDOS_USER} +CMD ["/bin/sh", "-c", "${RUNTIME_HOME}/entrypoint.sh"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8b156a6 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +include makefiles/const.mk +include makefiles/build-image.mk diff --git a/README.md b/README.md index 00fafa3..dab4336 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,95 @@ -# template-project -This is the template project for initializing other LTC projects. +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +![Tests](https://github.com/linktimecloud/kdp-catalog-manager/actions/workflows/unit-test.yml/badge.svg) +![Build](https://github.com/linktimecloud/kdp-catalog-manager/actions/workflows/ci-build.yml/badge.svg) +![](https://img.shields.io/badge/python-3.10.13-green) +![](https://img.shields.io/badge/fastapi-0.110.0-green) +![image version](https://img.shields.io/docker/v/linktimecloud/kdp-catalog-manager) +![image size](https://img.shields.io/docker/image-size/linktimecloud/kdp-catalog-manager) + + +# KDP Catalog Manager + +## 项目描述 + +### 项目概述 +KDP Catalog Manager是一套大数据应用管理平台。基于应用功能进行分类查看、管理,降低应用管理的复杂度,从而使大数据管理人员更专注于数据的处理 + +### 核心技术架构 +![kdp-catalog-manager](kdp-catalog-manager.png) + + + +### 功能模块描述 + +#### api +* view +定义 Restful API 并对用户输入参数执行基本验证及结果输出 + +#### Domain +* service +业务逻辑 +* format +数据转换层,用于缓存数据与业务数据转换 +* model +数据模型实体 + + +##### Modules +* cache +数据存储层,静态数据存储于缓存中 +* requests +外部数据调用,调用外部服务获取数据 + + +## 目录结构 +```shell +├── CODEOWNERS +├── README.md +└── kdp_catalog_manager +   ├── api +   ├── common +   ├── config +   ├── main.py # 服务启动程序 +   ├── requirements.txt +   ├── test_main.py +   ├── domain +   ├── modules +   └── utils +``` + +## 启动方式 +### 开发环境搭建 +* 使用python3.10+ + +1. 克隆代码至本地 +```shell +git clone xxx && cd kdp-catalog-manager +``` + +2. 使用虚拟环境 +```shell +#安装virtualenv +pip install virtualenv +virtualenv -p /usr/local/bin/python3 venv +# 激活虚拟环境 +source ./venv/bin/activate + +# 关闭虚拟环境 +deactivate +``` + +3. 安装依赖 +```shell +pip install -r docker/python/requirements.txt +``` + +4. 服务启动 +```shell +cd ~/kdp-catalog-manager \ +&& export PYTHONPATH=$PYTHONPATH:$(pwd) +python kdp_catalog_manager/main.py + +``` + +### API 手册 +* 启动服务后通过 http://127.0.0.1:8000/docs 查看提供的接口列表 \ No newline at end of file diff --git a/docker/bin/entrypoint.sh b/docker/bin/entrypoint.sh new file mode 100755 index 0000000..0ad362d --- /dev/null +++ b/docker/bin/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +declare RUNTIME_HOME=${RUNTIME_HOME:-"/opt/bdos/kdp/bdos-core"} +declare BDOS_USER=${BDOS_USER:-bdos} +declare CHOWN_HOST_PATH=${CHOWN_HOST_PATH:-"False"} +declare LOG_LEVEL=${LOG_LEVEL:-"info"} +declare WORKER_NUM=${WORKER_NUM:-4} +declare PARAMS_LIMIT=${PARAMS_LIMIT:-0} +declare MAX_REQUESTS=${MAX_REQUESTS:-100} +declare MAX_REQUESTS_JITTER=${MAX_REQUESTS:-100} + + +sudo chown ${BDOS_USER}:${BDOS_USER} ${RUNTIME_HOME} >/dev/null 2>&1 + +if [[ "${CHOWN_HOST_PATH}" == "True" ]];then + echo -e "### Change owner of $RUNTIME_HOME ###" + sudo chown -R ${BDOS_USER}:${BDOS_USER} ${RUNTIME_HOME}/kdp_catalog_manager ${RUNTIME_HOME}/logs ${RUNTIME_HOME} >/dev/null 2>&1 + echo -e "### Change owner return code: $? ###" +fi + + +echo -e "### Starting Server... ###" +cd ${RUNTIME_HOME} + +gunicorn -c kdp_catalog_manager/config/gunicorn.py kdp_catalog_manager.main:app --workers ${WORKER_NUM} --log-level ${LOG_LEVEL} --limit-request-line=${PARAMS_LIMIT} --max-requests ${MAX_REQUESTS} --max-requests-jitter ${MAX_REQUESTS_JITTER} --preload diff --git a/docker/python/requirements.txt b/docker/python/requirements.txt new file mode 100644 index 0000000..76ca52f --- /dev/null +++ b/docker/python/requirements.txt @@ -0,0 +1,8 @@ +fastapi[all]==0.110.0 +uvicorn[standard]==0.27.1 +cachelib==0.12.0 +concurrent-log-handler==0.9.25 +cachelib==0.12.0 +markdown==3.5.2 +prometheus-fastapi-instrumentator==6.1.0 +gunicorn==21.2.0 \ No newline at end of file diff --git a/kdp-catalog-manager.png b/kdp-catalog-manager.png new file mode 100644 index 0000000..17b4874 Binary files /dev/null and b/kdp-catalog-manager.png differ diff --git a/kdp_catalog_manager/__init__.py b/kdp_catalog_manager/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/api/__init__.py b/kdp_catalog_manager/api/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/api/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/api/api_v1/__init__.py b/kdp_catalog_manager/api/api_v1/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/api/api_v1/api.py b/kdp_catalog_manager/api/api_v1/api.py new file mode 100644 index 0000000..601fd85 --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/api.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from fastapi import APIRouter + +from kdp_catalog_manager.api.api_v1.endpoints import catalog +from kdp_catalog_manager.api.api_v1.endpoints import catalog_app_form +from kdp_catalog_manager.api.api_v1.endpoints import catalog_app_runtime + +api_router = APIRouter() + +# 分组路由 +api_router.include_router(catalog.router, tags=["Catalog"]) +api_router.include_router( + catalog_app_form.router, tags=["CatalogAppForm"] +) +api_router.include_router( + catalog_app_runtime.router, tags=["CatalogRuntime"] +) diff --git a/kdp_catalog_manager/api/api_v1/endpoints/__init__.py b/kdp_catalog_manager/api/api_v1/endpoints/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/endpoints/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/api/api_v1/endpoints/catalog.py b/kdp_catalog_manager/api/api_v1/endpoints/catalog.py new file mode 100644 index 0000000..c3197f8 --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/endpoints/catalog.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from typing import Annotated + +from fastapi import APIRouter, Path, Response, status, Header +from fastapi.responses import HTMLResponse + +from kdp_catalog_manager.common.constants import CATALOG_DESC, \ + NOT_FOUND_README_HTML +from kdp_catalog_manager.config.base_config import DEFAULT_LANG, SUPPORT_LANG +from kdp_catalog_manager.domain.model.catalog import CatalogList, \ + CatalogDataOut +from kdp_catalog_manager.domain.service.catalog import CatalogController +from kdp_catalog_manager.exceptions.exception import KdpCatalogManagerError, \ + FileNotExistsError, LangNotSupport +from kdp_catalog_manager.utils.format_return import FormatReturn + +router = APIRouter() + + +@router.get("/catalogs", + response_model=CatalogList, + summary="获取应用目录列表", + description="获取应用目录列表操作") +async def read_catalogs( + accept_language: Annotated[str, Header(description="请求语言类型")] = DEFAULT_LANG +): + """获取catalog 列表""" + try: + if accept_language not in SUPPORT_LANG: + raise LangNotSupport( + f"Header Accept-Language is:{accept_language}, " + f"not in {SUPPORT_LANG}") + data = CatalogController(lang=accept_language).get_catalogs() + rtn = FormatReturn().format_return_json( + data, msg="get catalogs data success") + except KdpCatalogManagerError as error: + error_info = FormatReturn().format_error_info( + error.error_name, + error.error_details, + error_msg=error.error_cname + ) + rtn = FormatReturn().format_return_json( + [], status=1, msg=error.error_cname, error_info=error_info) + return rtn + + +@router.get("/catalogs/{catalog}", + response_model=CatalogDataOut, + summary="获取应用目录信息", + description="获取某个应用目录信息操作") +async def read_catalog( + catalog: Annotated[str, Path(description=CATALOG_DESC)], + accept_language: Annotated[str, Header(description="请求语言类型")] = DEFAULT_LANG +): + """获取catalog 信息""" + try: + if accept_language not in SUPPORT_LANG: + raise LangNotSupport( + f"Header Accept-Language is:{accept_language}, " + f"not in {SUPPORT_LANG}") + data = CatalogController( + catalog=catalog, lang=accept_language).get_catalog() + rtn = FormatReturn().format_return_json( + data, msg="get catalogs data success") + except KdpCatalogManagerError as error: + error_info = FormatReturn().format_error_info( + error.error_name, + error.error_details, + error_msg=error.error_cname + ) + rtn = FormatReturn().format_return_json( + {}, status=1, msg=error.error_cname, error_info=error_info) + return rtn + + +@router.get("/catalogs/{catalog}/readme", + response_class=HTMLResponse, + summary="获取应用目录说明", + description="获取某个应用目录说明操作") +async def read_catalog_readme( + response: Response, + catalog: Annotated[str, Path(description=CATALOG_DESC)], + accept_language: Annotated[str, Header(description="请求语言类型")] = DEFAULT_LANG +): + """获取catalog 说明""" + try: + if accept_language not in SUPPORT_LANG: + raise LangNotSupport( + f"Header Accept-Language is:{accept_language}, " + f"not in {SUPPORT_LANG}") + rtn = CatalogController( + catalog, lang=accept_language).get_catalog_readme() + except FileNotExistsError: + response.status_code = status.HTTP_404_NOT_FOUND + rtn = NOT_FOUND_README_HTML + except LangNotSupport: + response.status_code = status.HTTP_400_BAD_REQUEST + rtn = "语言不支持:Header Accept-Language is:ens, not in ['zh', 'en']" + return rtn diff --git a/kdp_catalog_manager/api/api_v1/endpoints/catalog_app_form.py b/kdp_catalog_manager/api/api_v1/endpoints/catalog_app_form.py new file mode 100644 index 0000000..83d67da --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/endpoints/catalog_app_form.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from typing import Annotated + +from fastapi import APIRouter, Path, Query, Header, status, Response +from fastapi.responses import HTMLResponse + +from kdp_catalog_manager.common.constants import CATALOG_DESC, FORM_DESC, \ + ORG_DESC, BDC_DESC, NOT_FOUND_README_HTML +from kdp_catalog_manager.config.base_config import DEFAULT_LANG, SUPPORT_LANG +from kdp_catalog_manager.domain.model.catalog_app_form import CatalogFormList, \ + CatalogForm, CatalogFormInstall +from kdp_catalog_manager.domain.model.catalog_app_form import \ + Response as CatalogFormData +from kdp_catalog_manager.domain.service.catalog_form import \ + CatalogFormController +from kdp_catalog_manager.exceptions.exception import LangNotSupport, \ + FileNotExistsError, KdpCatalogManagerError +from kdp_catalog_manager.utils.format_return import FormatReturn + +router = APIRouter() + + +# +@router.get("/catalogs/{catalog}/app_forms", + response_model=CatalogFormList, + summary="获取应用模板列表", + description="获取应用模板列表操作") +async def read_catalog_forms( + catalog: Annotated[str, Path(description=CATALOG_DESC)], + accept_language: + Annotated[str, Header(description="请求语言类型")] = DEFAULT_LANG +): + """获取catalog 应用模板列表""" + try: + if accept_language not in SUPPORT_LANG: + raise LangNotSupport( + f"Header Accept-Language is:{accept_language}, " + f"not in {SUPPORT_LANG}") + data = CatalogFormController( + catalog=catalog, lang=accept_language + ).get_catalogs_forms() + rtn = FormatReturn().format_return_json( + data, msg="get catalog form templates success") + except KdpCatalogManagerError as error: + error_info = FormatReturn().format_error_info( + error.error_name, + error.error_details, + error_msg=error.error_cname + ) + rtn = FormatReturn().format_return_json( + [], status=1, msg=error.error_cname, error_info=error_info) + return rtn + + +@router.get("/catalogs/{catalog}/app_forms/{form}", + response_model=CatalogForm, + summary="获取应用模板信息", + description="获取某个应用模板信息操作") +async def read_catalog_form( + catalog: Annotated[str, Path(description=CATALOG_DESC)], + form: Annotated[str, Path(description=FORM_DESC)], + accept_language: + Annotated[str, Header(description="请求语言类型")] = DEFAULT_LANG +): + """获取catalog 应用模板信息""" + try: + if accept_language not in SUPPORT_LANG: + raise LangNotSupport( + f"Header Accept-Language is:{accept_language}, " + f"not in {SUPPORT_LANG}") + data = CatalogFormController( + catalog=catalog, app_form=form, lang=accept_language + ).get_catalogs_form() + rtn = FormatReturn().format_return_json( + data, msg="get catalog form template success") + except KdpCatalogManagerError as error: + error_info = FormatReturn().format_error_info( + error.error_name, + error.error_details, + error_msg=error.error_cname + ) + rtn = FormatReturn().format_return_json( + {}, status=1, msg=error.error_cname, error_info=error_info) + return rtn + + +@router.get("/catalogs/{catalog}/app_forms/{form}/data", + response_model=CatalogFormData, + summary="获取应用模板信息", + description="获取某个应用模板信息操作") +async def read_catalog_form_data( + catalog: Annotated[str, Path(description=CATALOG_DESC)], + form: Annotated[str, Path(description=FORM_DESC)], +): + """获取catalog 应用模板数据""" + try: + data = CatalogFormController( + catalog=catalog, app_form=form + ).get_catalog_form_data() + rtn = FormatReturn().format_return_json( + data, msg="get catalog form data success") + except KdpCatalogManagerError as error: + error_info = FormatReturn().format_error_info( + error.error_name, + error.error_details, + error_msg=error.error_cname + ) + rtn = FormatReturn().format_return_json( + {}, status=1, msg=error.error_cname, error_info=error_info) + return rtn + + +@router.get("/catalogs/{catalog}/app_forms/{form}/install", + response_model=CatalogFormInstall, + summary="获取应用模板应用安装信息", + description="获取某个应用模板应用安装信息操作") +async def read_catalog_form_install( + catalog: Annotated[str, Path(description=CATALOG_DESC)], + form: Annotated[str, Path(description=FORM_DESC)], + org: Annotated[str, Query(description=ORG_DESC)] = None, + bdc: Annotated[str, Query(description=BDC_DESC)] = None +): + """获取catalog 应用模板安装情况""" + try: + data = CatalogFormController( + catalog=catalog, app_form=form + ).get_catalog_forms_install(org, bdc) + rtn = FormatReturn().format_return_json( + data, msg="get catalog form install data success") + except KdpCatalogManagerError as error: + error_info = FormatReturn().format_error_info( + error.error_name, + error.error_details, + error_msg=error.error_cname + ) + rtn = FormatReturn().format_return_json( + [], status=1, msg=error.error_cname, error_info=error_info) + return rtn + + +@router.get("/catalogs/{catalog}/app_forms/{form}/readme", + response_class=HTMLResponse, + summary="获取应用模板使用说明信息", + description="获取某个应用模板使用说明信息操作") +async def read_catalog_form_readme( + response: Response, + catalog: Annotated[str, Path(description=CATALOG_DESC)], + form: Annotated[str, Path(description=FORM_DESC)], + accept_language: + Annotated[str, Header(description="请求语言类型")] = DEFAULT_LANG +): + """获取应用说明文档""" + try: + if accept_language not in SUPPORT_LANG: + raise LangNotSupport( + f"Header Accept-Language is:{accept_language}, " + f"not in {SUPPORT_LANG}") + rtn = CatalogFormController( + catalog, form, lang=accept_language).get_catalog_form_readme() + except FileNotExistsError: + response.status_code = status.HTTP_404_NOT_FOUND + rtn = NOT_FOUND_README_HTML + except LangNotSupport: + response.status_code = status.HTTP_400_BAD_REQUEST + rtn = "语言不支持:Header Accept-Language is:ens, not in ['zh', 'en']" + return rtn diff --git a/kdp_catalog_manager/api/api_v1/endpoints/catalog_app_runtime.py b/kdp_catalog_manager/api/api_v1/endpoints/catalog_app_runtime.py new file mode 100644 index 0000000..14fd18c --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/endpoints/catalog_app_runtime.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from fastapi import APIRouter, Query +from typing import Annotated + +from kdp_catalog_manager.common.constants import (CATALOG_DESC, + FORM_DESC, BDC_DESC) +from kdp_catalog_manager.domain.model.catalog_app_runtime import CatalogAppList +from kdp_catalog_manager.domain.service.application import ApplicationController +from kdp_catalog_manager.utils.format_return import FormatReturn +from kdp_catalog_manager.exceptions.exception import KdpCatalogManagerError + + +router = APIRouter() + + +@router.get("/catalogs/app_forms/apps", + response_model=CatalogAppList, + summary="获取应用列表信息", + description="获取应用列表信息操作") +async def read_catalog_runtimes( + catalog: Annotated[str, Query(description=CATALOG_DESC)] = None, + form: Annotated[str, Query(description=FORM_DESC)] = None, + bdc: Annotated[str, Query(description=BDC_DESC)] = None, + labelSelector: Annotated[ + str, Query( + description="A selector to restrict the list of " + "returned objects by their labels. " + "Defaults to everything." + ) + ] = None, +): + """获取catalog 运行态应用列表""" + try: + data = ApplicationController( + bdc=bdc, catalog=catalog, app_form=form + ).get_applications(labelSelector) + rtn = FormatReturn().format_return_json( + data, msg="get catalog form data success") + except KdpCatalogManagerError as error: + error_info = FormatReturn().format_error_info( + error.error_name, + error.error_details, + error_msg=error.error_cname + ) + rtn = FormatReturn().format_return_json( + [], status=1, msg=error.error_cname, error_info=error_info) + return rtn diff --git a/kdp_catalog_manager/api/api_v1/endpoints/tests/__init__.py b/kdp_catalog_manager/api/api_v1/endpoints/tests/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/endpoints/tests/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/api/api_v1/endpoints/tests/test_catalog.py b/kdp_catalog_manager/api/api_v1/endpoints/tests/test_catalog.py new file mode 100644 index 0000000..ad6ddc4 --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/endpoints/tests/test_catalog.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os.path + +from fastapi.testclient import TestClient +from unittest import TestCase +from kdp_catalog_manager.main import app +from kdp_catalog_manager.modules.cache.cache import cache_instance +from kdp_catalog_manager.common.constants import CATALOG_KEY +from kdp_catalog_manager.exceptions.exception import LangNotSupport + + +class TestCatalogApi(TestCase): + def setUp(self): + self.client = TestClient(app=app) + self.catalog_data = { + "mysql": { + "name": "mysql", + "category": "系统/大数据开发工具", + "description": "mysql", + "i18n": { + "en": { + "category": "system.dataManagement", + "description": "mysql" + } + } + } + } + self.catalog_readme_file = "readme/catalog/mysql/i18n/zh/README.html" + cache_instance.set(CATALOG_KEY, self.catalog_data) + self.get_catalogs_url = "/api/v1/catalogs" + self.get_catalogs_mysql_url = "/api/v1/catalogs/mysql" + + def test_get_catalogs(self): + response = self.client.get(self.get_catalogs_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 0) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data[0].keys() + self.assertEqual( + list(catalogs_data_keys), + ["name", "description", "category"] + ) + + def test_get_catalogs_en(self): + response = self.client.get( + self.get_catalogs_url, headers={"Accept-Language": "en"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 0) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data[0].keys() + self.assertEqual( + list(catalogs_data_keys), + ["name", "description", "category"] + ) + + def test_get_catalogs_not_found_lang(self): + response = self.client.get(self.get_catalogs_url, + headers={"Accept-Language": "zhs"}) + self.assertEqual(response.json().get("status"), 1) + self.assertEqual(response.status_code, 200) + + def test_get_catalogs_data_is_None(self): + self.catalog_data = None + cache_instance.set(CATALOG_KEY, self.catalog_data) + response = self.client.get(self.get_catalogs_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 1) + + def test_get_catalog(self): + response = self.client.get(self.get_catalogs_mysql_url) + self.assertEqual(response.status_code, 200) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data.keys() + self.assertEqual( + list(catalogs_data_keys), + ["name", "description", "category"] + ) + + def test_get_catalog_data_is_None(self): + self.catalog_data = None + cache_instance.set(CATALOG_KEY, self.catalog_data) + response = self.client.get(self.get_catalogs_mysql_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 1) + + def test_get_catalog_en(self): + response = self.client.get(self.get_catalogs_mysql_url, + headers={"Accept-Language": "en"}) + self.assertEqual(response.status_code, 200) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data.keys() + self.assertEqual( + list(catalogs_data_keys), + ["name", "description", "category"] + ) + + def test_get_catalog_not_found_lang(self): + response = self.client.get(self.get_catalogs_mysql_url, + headers={"Accept-Language": "ens"}) + self.assertEqual(response.status_code, 200) + + def test_catalog_readme(self): + response = self.client.get("/api/v1/catalogs/mysql/readme") + self.assertEqual(response.status_code, 404) + self.assertEqual( + response.headers.get("content-type"), + "text/html; charset=utf-8" + ) + + def test_catalog_readme_not_found(self): + response = self.client.get("/api/v1/catalogs/mysql1/readme") + self.assertEqual(response.status_code, 404) + self.assertEqual( + response.headers.get("content-type"), + "text/html; charset=utf-8" + ) + + def test_catalog_readme_not_found_lang(self): + response = self.client.get("/api/v1/catalogs/mysql1/readme", + headers={"Accept-Language": "ens"}) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.text, + "语言不支持:Header Accept-Language is:ens, not in ['zh', 'en']" + ) + + def test_catalog_readme_catalog(self): + response = self.client.get("/api/v1/catalogs//readme") + self.assertEqual(response.status_code, 404) + self.assertEqual( + response.headers.get("content-type"), + "application/json" + ) + + def test_catalog_readme_catalog_unsupport_lanf(self): + try: + self.client.get("/api/v1/catalogs/mysql/readme", + headers={"Accept-Language": "ens"}) + except LangNotSupport: + self.assertEqual(True, True) + + def tearDown(self): + cache_instance.clear() diff --git a/kdp_catalog_manager/api/api_v1/endpoints/tests/test_catalog_form.py b/kdp_catalog_manager/api/api_v1/endpoints/tests/test_catalog_form.py new file mode 100644 index 0000000..57c454c --- /dev/null +++ b/kdp_catalog_manager/api/api_v1/endpoints/tests/test_catalog_form.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os.path + +from fastapi.testclient import TestClient +from unittest import TestCase +from kdp_catalog_manager.main import app +from kdp_catalog_manager.modules.cache.cache import cache_instance +from kdp_catalog_manager.common.constants import CATALOG_FORM_KEY +from kdp_catalog_manager.exceptions.exception import KdpCatalogManagerError + + +class TestCatalogFormApi(TestCase): + def setUp(self): + self.client = TestClient(app=app) + self.catalog_form_metadata = { + "mysql": { + "mysql": { + "category": "系统/大数据开发工具", + "description": "mysql", + "i18n": { + "en": { + "category": "system.dataManagement", + "description": "mysql" + } + } + } + } + } + self.catalog_form_data = { + "apiVersion": "bdc.bdos.io/v1alpha1", + "kind": "Application" + } + self.catalog_from_readme_file = "readme/form/mysql/i18n/zh/README.html" + cache_instance.set(CATALOG_FORM_KEY, self.catalog_form_metadata) + self.get_catalogs_forms_url = "/api/v1/catalogs/mysql/app_forms" + self.get_catalogs_forms_mysql_url = "/api/v1/catalogs/mysql/app_forms/mysql" + self.get_catalog_form_mysql_data = "/api/v1/catalogs/mysql/app_forms/mysql/data" + self.rt_key_list = ["name", "version", "alias", "invisible", + "isGlobal", "description", "catalog", "dashboardUrl"] + + def test_get_catalogs_froms(self): + response = self.client.get(self.get_catalogs_forms_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 0) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data[0].keys() + self.assertEqual( + list(catalogs_data_keys), + self.rt_key_list + ) + + def test_get_catalogs_forms_en(self): + response = self.client.get( + self.get_catalogs_forms_url, headers={"Accept-Language": "en"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 0) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data[0].keys() + self.assertEqual( + list(catalogs_data_keys), + self.rt_key_list + ) + + def test_get_catalogs_forms_not_found_lang(self): + response = self.client.get(self.get_catalogs_forms_url, + headers={"Accept-Language": "zhs"}) + self.assertEqual(response.json().get("status"), 1) + self.assertEqual(response.status_code, 200) + + def test_get_catalogs_forms_data_is_None(self): + self.catalog_data = None + cache_instance.set(CATALOG_FORM_KEY, self.catalog_data) + response = self.client.get(self.get_catalogs_forms_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 1) + + def test_get_catalog_form(self): + response = self.client.get(self.get_catalogs_forms_mysql_url) + self.assertEqual(response.status_code, 200) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data.keys() + self.assertEqual( + list(catalogs_data_keys), + self.rt_key_list + ) + + def test_get_catalog_form_data_is_None(self): + self.catalog_data = None + cache_instance.set(CATALOG_FORM_KEY, self.catalog_data) + response = self.client.get(self.get_catalogs_forms_mysql_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 1) + + def test_get_catalog_form_en(self): + response = self.client.get(self.get_catalogs_forms_mysql_url, + headers={"Accept-Language": "en"}) + self.assertEqual(response.status_code, 200) + catalogs_data = response.json().get("data") + catalogs_data_keys = catalogs_data.keys() + self.assertEqual( + list(catalogs_data_keys), + self.rt_key_list + ) + + def test_get_catalog_form_not_found_lang(self): + response = self.client.get(self.get_catalogs_forms_mysql_url, + headers={"Accept-Language": "ens"}) + self.assertEqual(response.status_code, 200) + + def test_get_catalog_form_data(self): + cache_instance.set("mysql-mysql-data", self.catalog_form_data) + response = self.client.get(self.get_catalog_form_mysql_data) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json().get("data"), + self.catalog_form_data + ) + + # 缓存数据不存在 + cache_instance.delete("mysql-mysql-data") + response = self.client.get(self.get_catalog_form_mysql_data) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json().get("status"), 0) + + # url 异常 + response = self.client.get("/api/v1/catalogs//app_forms//data") + self.assertEqual(response.status_code, 404) + + def test_catalog_form_readme(self): + response = self.client.get("/api/v1/catalogs/mysql/app_forms/mysql/readme") + self.assertEqual(response.status_code, 404) + self.assertEqual(response.headers.get("content-type"), + "text/html; charset=utf-8" + ) + + def test_catalog_form_readme_not_found(self): + response = self.client.get( + "/api/v1/catalogs/mysql/app_forms/mysql1/readme", + headers={"Accept-Language": "zh"}) + self.assertEqual(response.status_code, 404) + self.assertEqual( + response.headers.get("content-type"), + "text/html; charset=utf-8" + ) + + def test_catalog_form_readme_not_found_lang(self): + response = self.client.get( + "/api/v1/catalogs/mysql/app_forms/mysql/readme", + headers={"Accept-Language": "ens"}) + self.assertEqual(response.status_code, 400) + self.assertEqual( + response.text, + "语言不支持:Header Accept-Language is:ens, not in ['zh', 'en']" + ) + + def test_catalog_form_readme_catalog(self): + response = self.client.get("/api/v1/catalogs/mysql/app_forms//readme") + self.assertEqual(response.status_code, 404) + self.assertEqual( + response.headers.get("content-type"), + "application/json" + ) + + def tearDown(self): + cache_instance.clear() diff --git a/kdp_catalog_manager/common/__init__.py b/kdp_catalog_manager/common/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/common/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/common/constants.py b/kdp_catalog_manager/common/constants.py new file mode 100644 index 0000000..81cc469 --- /dev/null +++ b/kdp_catalog_manager/common/constants.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +CATALOG_DESC = "应用目录名称" +FORM_DESC = "应用模板名称" +ORG_DESC = "机构名称" +BDC_DESC = "大数据集群名称" +LANG_DESC = "语言类型" + +METADATA_YAML = "metadata.yaml" +APP_YAML = "app.yaml" +README = "README.md" +I18N = "i18n" +README_HTML = "README.html" + +# cache key +CATALOG_KEY = "catalog_info" +CATALOG_FORM_KEY = "catalog_form" +# example: catalog-form +CATALOG_FROM_DATA_KEY = "{}-{}-data" + + +NOT_FOUND_README_HTML = """ + + + + 404 Not Found + + + +

404 Not Found

+

The resource could not be found.

+ + + + """ + + +# HTTP +HTTP_HEADER = { + "Accept": "application/json; charset=utf-8" +} +RESPONSE_NORMAL_CODE = 200 +RESPONSE_NOT_FOUND_CODE = 404 +RESPONSE_DATA = "data" diff --git a/kdp_catalog_manager/config/__init__.py b/kdp_catalog_manager/config/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/config/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/config/base_config.py b/kdp_catalog_manager/config/base_config.py new file mode 100644 index 0000000..1395f87 --- /dev/null +++ b/kdp_catalog_manager/config/base_config.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os + + +CACHE_EXPIRE = 60*60*24*365*10 + + +CATALOG_DIR = "catalog" +APPS_DIR = "apps" + +CATALOG_README_DIR = "readme/catalog" +CATALOG_FROM_README_DIR = "readme/form" + + +DEFAULT_LANG = "zh" +SUPPORT_LANG = (os.environ.get("SUPPORT_LANG") or "zh,en").split(",") + +# httpx connect other server setting +HTTP_TIME_OUT = int(os.environ.get("HTTP_TIME_OUT") or 10) +HTTP_MAX_RETRIES = int(os.environ.get("HTTP_MAX_RETRIES") or 3) +HTTP = "http" + + +# OAM config +OAM_BASE_URL = os.environ.get( + "OAM_BASE_URL", + default=f"{HTTP}://kdp-oam-apiserver:8000" +) + + +# markdown to html +EXTEND_EXTENSION = os.environ.get("EXTENSION", default=None) + + +# worker +WORKER_NUM = int(os.environ.get("WORKER_NUM") or 4) +LIMIT_MAX_REQUESTS = os.environ.get("LIMIT_MAX_REQUESTS", None) +TIMEOUT_KEEP_ALIVE = int(os.environ.get("TIMEOUT_KEEP_ALIVE") or 5) + + +# monitor +DASHBOARD_URL = os.environ.get("DASHBOARD_URL", default="https://grafana.com") diff --git a/kdp_catalog_manager/config/gunicorn.py b/kdp_catalog_manager/config/gunicorn.py new file mode 100644 index 0000000..2fcf9b5 --- /dev/null +++ b/kdp_catalog_manager/config/gunicorn.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +# 监听内网端口8000 +bind = "0.0.0.0:8888" +# 工作模式协程。 +worker_class = "uvicorn.workers.UvicornWorker" +# 不设置守护进程 +daemon = 'false' +# 日志输出到stdout、stderr +errorlog = '-' +accesslog = '-' +# 日志格式 +logconfig_dict = { + 'formatters': { + "generic": { + # 打日志的格式 + "format": "[%(asctime)s] [%(levelname)s] [PID:%(process)d][ThreadID:%(thread)d-%(threadName)s] %(message)s", + "class": "logging.Formatter" + } + } +} \ No newline at end of file diff --git a/kdp_catalog_manager/config/log_config.py b/kdp_catalog_manager/config/log_config.py new file mode 100644 index 0000000..7db9567 --- /dev/null +++ b/kdp_catalog_manager/config/log_config.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os + + +LOG_DIR = os.environ.get("LOG_DIR", "logs") +LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') +LOG_FILE = os.environ.get("LOG_FILE", "kdp-catalog-manager.log") +LOG_FILE_MAX_BYTES = os.environ.get('LOG_FILE_MAX_BYTES', 1024 * 1024 * 5) +LOG_FILE_BACKUP_COUNT = os.environ.get('LOG_FILE_BACKUP_COUNT', 3) + + +LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "[%(asctime)s] [%(levelname)s]: %(message)s", + "use_colors": None + }, + "access": { + "()": "uvicorn.logging.AccessFormatter", + "fmt": "[%(asctime)s] [%(levelname)s] [PID:%(process)d][ThreadID:%(thread)d-%(threadName)s] - [%(status_code)s]: %(message)s" + } + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr" + }, + "access": { + "formatter": "access", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr" + } + }, + "loggers": { + "uvicorn": { + "handlers": ["default"], + "level": "ERROR", + "propagate": False + }, + "uvicorn.error": { + "level": "INFO" + }, + "uvicorn.access": { + "handlers": ["access"], + "level": "INFO", + "propagate": False + } + } +} \ No newline at end of file diff --git a/kdp_catalog_manager/domain/__init__.py b/kdp_catalog_manager/domain/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/domain/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/domain/format/__init__.py b/kdp_catalog_manager/domain/format/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/domain/format/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/domain/format/base.py b/kdp_catalog_manager/domain/format/base.py new file mode 100644 index 0000000..0e2b3b4 --- /dev/null +++ b/kdp_catalog_manager/domain/format/base.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + + +class Format(object): + def __init__(self, raw): + self.raw = raw diff --git a/kdp_catalog_manager/domain/format/format_catalog.py b/kdp_catalog_manager/domain/format/format_catalog.py new file mode 100644 index 0000000..0905f13 --- /dev/null +++ b/kdp_catalog_manager/domain/format/format_catalog.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from kdp_catalog_manager.domain.format.base import Format +from kdp_catalog_manager.utils.dictutils import DictUtils +from kdp_catalog_manager.config.base_config import DEFAULT_LANG +from kdp_catalog_manager.common.constants import I18N + + +class FormatCatalog(Format): + def __init__(self, raw): + super().__init__(raw) + + def get_name(self): + return DictUtils().get_items(self.raw, ["name"]) + + def get_type(self): + return DictUtils().get_items(self.raw, ["isGlobal"], False) + + def get_category(self, lang=DEFAULT_LANG): + if lang == DEFAULT_LANG: + return DictUtils().get_items(self.raw, ["category"]) + return DictUtils().get_items(self.raw, [I18N, lang, "category"]) + + def get_description(self, lang=DEFAULT_LANG): + if lang == DEFAULT_LANG: + return DictUtils().get_items(self.raw, ["description"]) + return DictUtils().get_items(self.raw, [I18N, lang, "description"]) diff --git a/kdp_catalog_manager/domain/format/format_catalog_form.py b/kdp_catalog_manager/domain/format/format_catalog_form.py new file mode 100644 index 0000000..5d4dfb4 --- /dev/null +++ b/kdp_catalog_manager/domain/format/format_catalog_form.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from kdp_catalog_manager.common.constants import I18N +from kdp_catalog_manager.config.base_config import DEFAULT_LANG +from kdp_catalog_manager.domain.format.base import Format +from kdp_catalog_manager.utils.dictutils import DictUtils +from kdp_catalog_manager.config.base_config import DASHBOARD_URL + + +class FormatCatalogForm(Format): + def __init__(self, raw): + super().__init__(raw) + + def get_version(self): + return DictUtils().get_items(self.raw, ["version"]) + + def get_type(self): + return DictUtils().get_items(self.raw, ["isGlobal"], False) + + def get_alias(self): + return DictUtils().get_items(self.raw, ["alias"]) + + def get_invisible(self): + return DictUtils().get_items(self.raw, ["invisible"], False) + + def get_description(self, lang=DEFAULT_LANG): + if lang == DEFAULT_LANG: + return DictUtils().get_items(self.raw, ["description"]) + return DictUtils().get_items( + self.raw, [I18N, lang, "description"]) + + def get_dashboard(self, lang=DEFAULT_LANG): + """ + get catalog from dashboard info + :param lang: + :return: [{"name": "xx", "id": "xxx"}] + """ + dashboard_list = [] + dashboards = DictUtils().get_items(self.raw, ["dashboard"]) + if not dashboards: + return dashboard_list + for dashboard in dashboards: + dashboard_id = DictUtils().get_items(dashboard, ["id"]) + dashboard_name = None + if lang == DEFAULT_LANG: + dashboard_name = DictUtils().get_items( + dashboard, ["name"]) + if lang != DEFAULT_LANG: + dashboard_name = DictUtils().get_items( + dashboard, [I18N, lang]) + dashboard_list.append({ + "name": dashboard_name, + "id": dashboard_id, + "link": f"{DASHBOARD_URL}/d/{dashboard_id}" + }) + return dashboard_list + + +def format_catalog_form_metadata(catalog_form_metadatas, filter_lang, filter_invisible): + catalog_form_metadata_info = {} + catalog_form_metadata_all = catalog_form_metadatas + for catalog, catalog_form_metadatas in catalog_form_metadata_all.items(): + + catalog_catalog_form_metadatas_info = {} + for catalog_form, catalog_form_metadata in catalog_form_metadatas.items(): + + catalog_form_metadata_format_obj = FormatCatalogForm( + catalog_form_metadata) + invisible = catalog_form_metadata_format_obj.get_invisible() + if filter_invisible != invisible: + continue + catalog_catalog_form_metadatas_info[catalog_form] = { + "name": catalog_form, + "version": catalog_form_metadata_format_obj.get_version(), + "alias": catalog_form_metadata_format_obj.get_alias(), + "invisible": catalog_form_metadata_format_obj.get_invisible(), + "isGlobal": catalog_form_metadata_format_obj.get_type(), + "description": catalog_form_metadata_format_obj.get_description( + filter_lang), + "catalog": catalog, + "dashboardUrl": catalog_form_metadata_format_obj.get_dashboard( + filter_lang) + } + catalog_form_metadata_info[ + catalog] = catalog_catalog_form_metadatas_info + return catalog_form_metadata_info diff --git a/kdp_catalog_manager/domain/format/format_catalog_runtime_application.py b/kdp_catalog_manager/domain/format/format_catalog_runtime_application.py new file mode 100644 index 0000000..2d09a69 --- /dev/null +++ b/kdp_catalog_manager/domain/format/format_catalog_runtime_application.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from kdp_catalog_manager.domain.format.base import Format +from kdp_catalog_manager.utils.dictutils import DictUtils +from kdp_catalog_manager.utils.log import log + + +class FormatCatalogRuntimeApplication(Format): + def __init__(self, raw): + super().__init__(raw) + + def get_bdc_name(self): + return DictUtils().get_items(self.raw, ["bdc", "name"]) + + def get_bdc_org(self): + return DictUtils().get_items(self.raw, ["bdc", "orgName"]) + + def get_bdc_status(self): + return DictUtils().get_items(self.raw, ["bdc", "status"]) + + def get_app_name(self): + return DictUtils().get_items(self.raw, ["name"]) + + def get_app_form(self): + return DictUtils().get_items(self.raw, ["appFormName"]) + + def get_app_runtime_name(self): + return DictUtils().get_items(self.raw, ["appRuntimeName"]) + + def get_app_createtime(self): + return DictUtils().get_items(self.raw, ["createTime"]) + + def get_app_updatetime(self): + return DictUtils().get_items(self.raw, ["updateTime"]) + + def get_app_status(self): + return DictUtils().get_items(self.raw, ["status"]) + + +def format_application( + all_application, + form_info, + filter_catalog, + filter_form +): + applications = [] + # deal with get application data is empty list or None + if not all_application: + return applications + for application_data in all_application: + application = application_data + application_format_obj = ( + FormatCatalogRuntimeApplication(application)) + application_form = application_format_obj.get_app_form() + application_catalog = DictUtils().get_items( + form_info, [application_form]) + + if not application_catalog: + log.warning(f"{application_form} not match catalog, " + f"get data is {application_catalog}") + continue + + # filter application + if filter_catalog and filter_catalog != application_catalog: + continue + if filter_form and filter_form != application_form: + continue + + application["catalog"] = application_catalog + applications.append(application) + return applications diff --git a/kdp_catalog_manager/domain/format/test/__init__.py b/kdp_catalog_manager/domain/format/test/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/domain/format/test/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/domain/format/test/test_format_catalog.py b/kdp_catalog_manager/domain/format/test/test_format_catalog.py new file mode 100644 index 0000000..b39ebcf --- /dev/null +++ b/kdp_catalog_manager/domain/format/test/test_format_catalog.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from unittest import TestCase + +from ..format_catalog import FormatCatalog + +TEST_SYSTEM_CATEGORY = "系统/大数据开发工具" + + +class TestFormCatalog(TestCase): + def setUp(self): + self.catalog_data = { + "name": "mysql", + "category": "系统/大数据开发工具", + "description": "mysql", + "i18n": { + "en": { + "category": "system.dataManagement", + "description": "Mysql M" + } + } + } + self.catalog_format_obj = FormatCatalog(self.catalog_data) + + def test_get_name(self): + rt = self.catalog_format_obj.get_name() + self.assertEqual(rt, "mysql") + + def test_get_type(self): + rt = self.catalog_format_obj.get_type() + self.assertEqual(rt, False) + + def test_get_type_exists(self): + self.catalog_format_obj.raw["isGlobal"] = True + rt = self.catalog_format_obj.get_type() + self.assertEqual(rt, True) + + def test_get_category_default(self): + rt = self.catalog_format_obj.get_category("zh") + self.assertEqual(rt, TEST_SYSTEM_CATEGORY) + + def test_get_category_en(self): + rt = self.catalog_format_obj.get_category("en") + self.assertEqual(rt, "system.dataManagement") + + def test_get_category(self): + rt = self.catalog_format_obj.get_category() + self.assertEqual(rt, TEST_SYSTEM_CATEGORY) + + def test_get_description(self): + rt = self.catalog_format_obj.get_description() + self.assertEqual(rt, "mysql") + + def test_get_description_zh(self): + rt = self.catalog_format_obj.get_description("zh") + self.assertEqual(rt, "mysql") + + def test_get_description_en(self): + rt = self.catalog_format_obj.get_description("en") + self.assertEqual(rt, "Mysql M") diff --git a/kdp_catalog_manager/domain/format/test/test_format_catalog_form.py b/kdp_catalog_manager/domain/format/test/test_format_catalog_form.py new file mode 100644 index 0000000..d2a3eba --- /dev/null +++ b/kdp_catalog_manager/domain/format/test/test_format_catalog_form.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from unittest import TestCase + +from ..format_catalog_form import FormatCatalogForm + + +TEST_DESC = "mysql test" + + +class TestFormatCatalogForm(TestCase): + def setUp(self): + self.catalog_form_metadata = { + "version": "1.0.0", + "alias": "mysql", + "description": TEST_DESC, + "isGlobal": False, + "i18n": { + "en": { + "category": "system.dataManagement", + "description": "Mysql M" + } + } + } + self.dashboard = [{ + "id": "xxx", + "name": "x1", + "i18n": { + "en": "x11" + } + }] + self.catalog_form_format_obj = FormatCatalogForm(self.catalog_form_metadata) + + def test_get_version(self): + rt = self.catalog_form_format_obj.get_version() + self.assertEqual(rt, "1.0.0") + + def test_get_version_not_exists(self): + del self.catalog_form_format_obj.raw["version"] + rt = self.catalog_form_format_obj.get_version() + self.assertEqual(rt, None) + + def test_get_type(self): + rt = self.catalog_form_format_obj.get_type() + self.assertEqual(rt, False) + + def test_get_type_exists(self): + self.catalog_form_format_obj.raw["isGlobal"] = True + rt = self.catalog_form_format_obj.get_type() + self.assertEqual(rt, True) + + def test_get_alias(self): + rt = self.catalog_form_format_obj.get_alias() + self.assertEqual(rt, "mysql") + + def test_get_invisible(self): + rt = self.catalog_form_format_obj.get_invisible() + self.assertEqual(rt, False) + + def test_get_invisible_exists(self): + self.catalog_form_format_obj.raw["invisible"] = True + rt = self.catalog_form_format_obj.get_invisible() + self.assertEqual(rt, True) + + def test_get_description(self): + rt = self.catalog_form_format_obj.get_description() + self.assertEqual(rt, TEST_DESC) + + def test_get_description_zh(self): + rt = self.catalog_form_format_obj.get_description("zh") + self.assertEqual(rt, TEST_DESC) + + def test_get_description_en(self): + rt = self.catalog_form_format_obj.get_description("en") + self.assertEqual(rt, "Mysql M") + + def test_dashboard(self): + rt = self.catalog_form_format_obj.get_dashboard() + self.assertEqual(rt, []) + + def test_dashboard_exists(self): + self.catalog_form_format_obj.raw["dashboard"] = self.dashboard + rt = self.catalog_form_format_obj.get_dashboard() + self.assertEqual(rt, [{ + "id": "xxx", "name": "x1", 'link': 'https://grafana.com/d/xxx'}]) + + def test_dashboard_exists_en(self): + self.catalog_form_format_obj.raw["dashboard"] = self.dashboard + rt = self.catalog_form_format_obj.get_dashboard(lang="en") + self.assertEqual(rt, [{ + "id": "xxx", "name": "x11", 'link': 'https://grafana.com/d/xxx'}]) diff --git a/kdp_catalog_manager/domain/format/test/test_format_catalog_runtime_application.py b/kdp_catalog_manager/domain/format/test/test_format_catalog_runtime_application.py new file mode 100644 index 0000000..32e4ad2 --- /dev/null +++ b/kdp_catalog_manager/domain/format/test/test_format_catalog_runtime_application.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from unittest import TestCase + +from ..format_catalog_runtime_application import FormatCatalogRuntimeApplication, format_application + + +TEST_TIME = "0001-01-01T00:00:00Z" + + +CATALOG_RUNTIME_APPLICATION = { + "name": "admin-admin-bdos-file-registry", + "appFormName": "mysql", + "appTemplateType": "file-registry", + "appRuntimeName": "", + "appRuntimeNs": "admin", + "bdc": { + "name": "admin-admin", + "alias": "", + "description": "", + "orgName": "admin", + "status": "" + }, + "createTime": TEST_TIME, + "updateTime": TEST_TIME, + "labels": { + "app": "bdos-file-registry", + "app.core.bdos/type": "system", + "bdc.bdos.io/name": "admin-admin", + "bdc.bdos.io/org": "admin" + }, + "status": "running" +} + + +class TestFormatCatalogRuntimeApplication(TestCase): + def setUp(self): + self.maxDiff = None + self.catalog_runtime_application_data = CATALOG_RUNTIME_APPLICATION + self.catalog_runtime_application_format_obj = ( + FormatCatalogRuntimeApplication( + self.catalog_runtime_application_data + ) + ) + + def test_get_bdc_name(self): + rt = self.catalog_runtime_application_format_obj.get_bdc_name() + self.assertEqual(rt, "admin-admin") + + def test_get_bdc_org(self): + rt = self.catalog_runtime_application_format_obj.get_bdc_org() + self.assertEqual(rt, "admin") + + def test_get_bdc_status(self): + rt = self.catalog_runtime_application_format_obj.get_bdc_status() + self.assertEqual(rt, "") + + def test_get_app_name(self): + rt = self.catalog_runtime_application_format_obj.get_app_name() + self.assertEqual(rt, "admin-admin-bdos-file-registry") + + def test_get_app_form(self): + rt = self.catalog_runtime_application_format_obj.get_app_form() + self.assertEqual(rt, "mysql") + + def test_get_app_runtime_name(self): + rt = self.catalog_runtime_application_format_obj.get_app_runtime_name() + self.assertEqual(rt, "") + + def test_get_app_createtime(self): + rt = self.catalog_runtime_application_format_obj.get_app_createtime() + self.assertEqual(rt, TEST_TIME) + + def test_get_app_updatetime(self): + rt = self.catalog_runtime_application_format_obj.get_app_updatetime() + self.assertEqual(rt, TEST_TIME) + + def test_get_app_status(self): + rt = self.catalog_runtime_application_format_obj.get_app_status() + self.assertEqual(rt, "running") + + +class TestFormatApplication(TestCase): + def setUp(self): + self.maxDiff = None + self.all_application = CATALOG_RUNTIME_APPLICATION + self.form_info = {"mysql": "mysql", "spark": "spark"} + self.filter_catalog = None + self.filter_form = None + + self.rt = CATALOG_RUNTIME_APPLICATION + self.add_application = { + "appFormName": "test", + "bdc": { + "name": "", + "orgName": "admin" + } + } + + def test_format_application(self): + rt = format_application( + [self.all_application], + self.form_info, + self.filter_catalog, + self.filter_form + ) + self.assertEqual(rt, [self.rt]) + + def test_format_application_not_match_catalog(self): + rt = format_application( + [self.all_application, self.add_application], + self.form_info, + self.filter_catalog, + self.filter_form + ) + self.assertEqual(rt, [self.rt]) + + def test_format_application_filter_catalog(self): + self.add_application["appFormName"] = "spark" + rt = format_application( + [self.all_application, self.add_application], + self.form_info, + "mysql", + self.filter_form + ) + self.assertEqual(rt, [self.rt]) + + def test_format_application_filter_catalog1(self): + self.add_application["appFormName"] = "spark" + rt = format_application( + [self.all_application, self.add_application], + self.form_info, + "spark", + self.filter_form + ) + self.assertEqual(rt, [self.add_application]) + + def test_format_application_filter_catalog_form(self): + self.add_application["appFormName"] = "spark" + rt = format_application( + [self.all_application, self.add_application], + self.form_info, + self.filter_catalog, + "mysql" + ) + self.assertEqual(rt, [self.rt]) diff --git a/kdp_catalog_manager/domain/model/__init__.py b/kdp_catalog_manager/domain/model/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/domain/model/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/domain/model/base.py b/kdp_catalog_manager/domain/model/base.py new file mode 100644 index 0000000..3f6ef3b --- /dev/null +++ b/kdp_catalog_manager/domain/model/base.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from pydantic import BaseModel, Field + + +class Response(BaseModel): + status: int = Field(default=0, description="状态码, 1:失败, 0:成功") + message: str = Field(description="描述信息") + data: dict = Field(description="返回数据") + error: dict = Field(description="错误信息") diff --git a/kdp_catalog_manager/domain/model/catalog.py b/kdp_catalog_manager/domain/model/catalog.py new file mode 100644 index 0000000..793f1b7 --- /dev/null +++ b/kdp_catalog_manager/domain/model/catalog.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from typing import List, Dict + +from pydantic import BaseModel, Field + +from kdp_catalog_manager.domain.model.base import Response + + +class CatalogData(BaseModel): + name: str = Field(description="应用目录") + description: str = Field(description="应用目录说明") + category: str = Field(description="应用目录分类") + + +class CatalogDataOut(Response): + data: CatalogData | Dict + + +class CatalogList(Response): + data: List[CatalogData] | List diff --git a/kdp_catalog_manager/domain/model/catalog_app_form.py b/kdp_catalog_manager/domain/model/catalog_app_form.py new file mode 100644 index 0000000..55ada91 --- /dev/null +++ b/kdp_catalog_manager/domain/model/catalog_app_form.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from typing import List, Dict + +from pydantic import BaseModel, Field + +from kdp_catalog_manager.domain.model.base import Response + + +class CatalogFormDataDashboardUrl(BaseModel): + name: str = Field(description="面板名称") + id: str = Field(description="面板id") + link: str = Field(description="面板地址") + + +class CatalogFormData(BaseModel): + name: str | None = Field(description="应用模板名称") + version: str | None = Field(description="应用模板版本") + alias: str | None = Field(description="应用模板别名") + invisible: bool = Field( + default=False, description="应用模板是否隐藏") + isGlobal: bool = Field(default=False, description="是否为全局级别") + description: str | None = Field(description="应用模板说明") + catalog: str = Field(description="应用目录") + dashboardUrl: List[CatalogFormDataDashboardUrl] | List + + +class CatalogFormList(Response): + data: List[CatalogFormData] | List + + +class CatalogForm(Response): + data: CatalogFormData | Dict + + +class CatalogFormInstallDataList(BaseModel): + org: str = Field(description="机构名称") + bdc: list = [] + + +class CatalogFormInstallData(BaseModel): + name: str = Field(description="form名称") + installtion: List[CatalogFormInstallDataList] + + +class CatalogFormInstall(Response): + data: CatalogFormInstallData diff --git a/kdp_catalog_manager/domain/model/catalog_app_runtime.py b/kdp_catalog_manager/domain/model/catalog_app_runtime.py new file mode 100644 index 0000000..d6212b7 --- /dev/null +++ b/kdp_catalog_manager/domain/model/catalog_app_runtime.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from typing import List + +from pydantic import BaseModel, Field + +from kdp_catalog_manager.domain.model.base import Response + + +class CatalogAppListDataMetadata(BaseModel): + catalog: str = Field(description="应用目录名称") + category: str = Field(description="应用目录分类") + appForm: str = Field(description="应用模板名称") + appName: str = Field(description="应用名称") + appRuntime: str = Field(description="应用实例名称") + org: str = Field(description="机构名称") + bdc: str = Field(description="大数据集群名称") + + +class CatalogAppListData(BaseModel): + name: str = Field(description="应用名称") + isGlobal: bool = Field(default=False, description="是否为全局级别") + status: str = Field(description="应用状态") + updataTime: str = Field(description="应用更新时间") + metadata: CatalogAppListDataMetadata + + +class CatalogAppList(Response): + data: List diff --git a/kdp_catalog_manager/domain/service/__init__.py b/kdp_catalog_manager/domain/service/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/domain/service/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/domain/service/application.py b/kdp_catalog_manager/domain/service/application.py new file mode 100644 index 0000000..159216a --- /dev/null +++ b/kdp_catalog_manager/domain/service/application.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from kdp_catalog_manager.domain.format.format_catalog_runtime_application import \ + format_application +from kdp_catalog_manager.domain.service.catalog_form import \ + CatalogFormController +from kdp_catalog_manager.exceptions.exception import GetDataError, \ + get_exception +from kdp_catalog_manager.modules.http_requests.application.oam_application import \ + OamApplication +from kdp_catalog_manager.utils.log import log + + +class ApplicationController(object): + def __init__(self, bdc=None, catalog=None, app_form=None): + self.bdc = bdc + self.catalog = catalog + self.app_form = app_form + + def get_applications(self, label_selector): + try: + form_info = CatalogFormController().get_form_info() + except Exception: + log.error(get_exception()) + raise GetDataError("get form catalog error") + + params = {} + if self.bdc: + params["bdcName"] = self.bdc + if label_selector: + params["labelSelector"] = label_selector + if not params: + params = None + all_application = OamApplication().list_all(params) + applications = format_application( + all_application, form_info, self.catalog, self.app_form) + return applications diff --git a/kdp_catalog_manager/domain/service/catalog.py b/kdp_catalog_manager/domain/service/catalog.py new file mode 100644 index 0000000..60630b1 --- /dev/null +++ b/kdp_catalog_manager/domain/service/catalog.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os.path + +from kdp_catalog_manager.common.constants import CATALOG_KEY, I18N, README_HTML +from kdp_catalog_manager.config.base_config import DEFAULT_LANG, \ + CATALOG_README_DIR +from kdp_catalog_manager.domain.format.format_catalog import FormatCatalog +from kdp_catalog_manager.exceptions.exception import DataTypeError, \ + APIRequestedInvalidParamsError +from kdp_catalog_manager.modules.cache.cache import cache_instance +from kdp_catalog_manager.utils.dictutils import DictUtils +from kdp_catalog_manager.utils.fileutils import FileUtils + + +class CatalogController(object): + def __init__(self, catalog=None, lang=DEFAULT_LANG): + self.catalog = catalog + self.lang = lang + + def get_catalogs_info(self): + catalogs_info = {} + catalogs_data = cache_instance.get(CATALOG_KEY) + if not isinstance(catalogs_data, dict): + raise DataTypeError(f"data type is {type(catalogs_data)}") + for catalog, catalog_info in catalogs_data.items(): + if not catalog_info: + catalogs_info[catalog] = {} + continue + catalog_format_obj = FormatCatalog(catalog_info) + catalogs_info[catalog] = { + "name": catalog_format_obj.get_name(), + "description": catalog_format_obj.get_description(self.lang), + "category": catalog_format_obj.get_category(self.lang) + } + return catalogs_info + + def get_catalogs(self): + catalogs = [] + catalogs_info = self.get_catalogs_info() + if not catalogs_info: + return catalogs + for catalog_info in catalogs_info.values(): + catalogs.append(catalog_info) + return catalogs + + def get_catalog(self): + catalogs_info = self.get_catalogs_info() + catalog_data = DictUtils().get_items(catalogs_info, [self.catalog], {}) + return catalog_data + + def get_catalog_readme(self): + if self.catalog is None: + raise APIRequestedInvalidParamsError("catalog is None") + catalog_file = os.path.join( + CATALOG_README_DIR, + self.catalog, + I18N, + self.lang, + README_HTML + ) + return FileUtils().read_file(catalog_file) diff --git a/kdp_catalog_manager/domain/service/catalog_form.py b/kdp_catalog_manager/domain/service/catalog_form.py new file mode 100644 index 0000000..cb21a7a --- /dev/null +++ b/kdp_catalog_manager/domain/service/catalog_form.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os + +from kdp_catalog_manager.common.constants import CATALOG_FORM_KEY, \ + CATALOG_FROM_DATA_KEY, I18N, README_HTML +from kdp_catalog_manager.config.base_config import DEFAULT_LANG, \ + CATALOG_FROM_README_DIR +from kdp_catalog_manager.domain.format.format_catalog_form import \ + FormatCatalogForm, format_catalog_form_metadata +from kdp_catalog_manager.exceptions.exception import CacheNotExistsError, \ + CatalogFormMatchError, APIRequestedInvalidParamsError +from kdp_catalog_manager.modules.cache.cache import cache_instance +from kdp_catalog_manager.utils.dictutils import DictUtils +from kdp_catalog_manager.utils.fileutils import FileUtils +from kdp_catalog_manager.modules.http_requests.application.oam_application import OamApplication +from kdp_catalog_manager.domain.format.format_catalog_runtime_application import FormatCatalogRuntimeApplication +from kdp_catalog_manager.utils.log import log + + +class CatalogFormController(object): + def __init__( + self, catalog=None, app_form=None, + lang=DEFAULT_LANG, invisible=False + ): + self.catalog = catalog + self.app_form = app_form + self.lang = lang + self.invisible = invisible + + def get_catalog_form_metadata_for_cache(self): + """ + get catalog from metadata from cache + :return: + """ + catalog_form_metadatas = cache_instance.get(CATALOG_FORM_KEY) + if not catalog_form_metadatas: + raise CacheNotExistsError( + "catalog from cache not exists, please re-initialize") + return catalog_form_metadatas + + def get_format_catalog_metadata(self): + """ + get format catalog + :return: + { + "catalog": + { + "catalog_form": + { + "name": "xxxx", + "version": "", + "alias": "Mysql", + "isGlobal": false, + "description": "xxx", + "invisible": false + } + } + } + """ + catalog_form_metadata_cache = self.get_catalog_form_metadata_for_cache() + catalog_form_metadata = format_catalog_form_metadata( + catalog_form_metadata_cache, + self.lang, + self.invisible + ) + return catalog_form_metadata + + def get_catalogs_forms(self): + catalog_forms = [] + catalog_form_metadata = self.get_format_catalog_metadata() + catalog_forms_metadta = DictUtils().get_items( + catalog_form_metadata, self.catalog) + if not catalog_forms_metadta: + return catalog_forms + for catalog_form_info in catalog_forms_metadta.values(): + catalog_forms.append(catalog_form_info) + return catalog_forms + + def get_catalogs_form(self): + catalog_form = {} + catalog_form_metadata = self.get_format_catalog_metadata() + catalog_forms_metadta = DictUtils().get_items( + catalog_form_metadata, self.catalog) + if not catalog_forms_metadta: + return catalog_form + + catalog_form = DictUtils().get_items( + catalog_forms_metadta, [self.app_form]) + if not catalog_form: + raise CatalogFormMatchError( + f"{self.catalog} not found {self.app_form}") + + return catalog_form + + def get_catalog_form_data(self): + catalog_form_data = cache_instance.get( + CATALOG_FROM_DATA_KEY.format(self.catalog, self.app_form) + ) + if not catalog_form_data: + return {} + return catalog_form_data + + def get_catalog_forms_install(self, org=None, bdc=None): + params = None + if bdc: + params = { + "bdcName": bdc + } + form_application_install = {} + all_application_data = OamApplication().list_all(params) + # deal with get application data is empty list or None + if all_application_data: + for application_data in all_application_data: + if not application_data: + continue + application_format_obj = ( + FormatCatalogRuntimeApplication(application_data)) + + application_org = application_format_obj.get_bdc_org() + application_bdc = application_format_obj.get_bdc_name() + application_form = application_format_obj.get_app_form() + if self.app_form != application_form: + continue + if org and org != application_org: + continue + + if application_org not in form_application_install: + form_application_install[application_org] = [application_bdc] + continue + if application_bdc not in form_application_install[application_org]: + form_application_install[application_org].append(application_bdc) + + install_data = [] + if form_application_install: + for org, bdc in form_application_install.items(): + install_data.append({ + "org": org, + "bdc": bdc + }) + + catalog_forms_install = { + "name": self.app_form, + "installtion": install_data + } + return catalog_forms_install + + def get_catalog_form_readme(self): + if self.catalog is None or self.app_form is None: + raise APIRequestedInvalidParamsError("catalog or app form is None") + catalog_file = os.path.join( + CATALOG_FROM_README_DIR, + self.app_form, + I18N, + self.lang, + README_HTML + ) + return FileUtils().read_file(catalog_file) + + def get_form_info(self): + """ + get form info + :return: {"form_name": "catalog_name"} + """ + form_info = {} + catalog_form_metadata = self.get_format_catalog_metadata() + for catalog, catalog_forms in catalog_form_metadata.items(): + for catalog_form in catalog_forms.keys(): + form_info[catalog_form] = catalog + return form_info diff --git a/kdp_catalog_manager/domain/service/markdown_transform_html.py b/kdp_catalog_manager/domain/service/markdown_transform_html.py new file mode 100644 index 0000000..8de02b0 --- /dev/null +++ b/kdp_catalog_manager/domain/service/markdown_transform_html.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os + +from kdp_catalog_manager.common.constants import README, I18N +from kdp_catalog_manager.config.base_config import CATALOG_DIR, APPS_DIR, \ + CATALOG_README_DIR, CATALOG_FROM_README_DIR, DEFAULT_LANG +from kdp_catalog_manager.utils.log import log +from kdp_catalog_manager.utils.markdown_to_html.markdownToHtml import \ + MarkdownToHtlm + + +def deal_readme(base_dir, output_file, output_base_dir=CATALOG_README_DIR): + for base_dir_name in os.listdir(base_dir): + form_name = base_dir_name + if base_dir_name.endswith(".app"): + form_name = base_dir_name.split(".")[0] + + # 处理默认语言readme + default_reademe_file = os.path.join(base_dir, base_dir_name, README) + output_dir = os.path.join( + output_base_dir, form_name, I18N, DEFAULT_LANG) + MarkdownToHtlm().markdown_to_html( + default_reademe_file, + output_file, + output_dir + ) + + # 处理其他语言readme + readme_lang_dir = os.path.join(base_dir, base_dir_name, I18N) + for readme_lang in os.listdir(readme_lang_dir): + readme_lang_file = os.path.join(readme_lang_dir, readme_lang, README) + output_dir = os.path.join( + output_base_dir, form_name, I18N, readme_lang + ) + MarkdownToHtlm().markdown_to_html( + readme_lang_file, + output_file, + output_dir + ) + + +def catalog_readme(): + output_file = README.replace("md", "html") + deal_readme(CATALOG_DIR, output_file, CATALOG_README_DIR) + log.info("transform catalog readme to html success") + + +def catalog_form_readme(): + output_file = README.replace("md", "html") + for catalog in os.listdir(CATALOG_DIR): + catalog_form_dir = os.path.join(CATALOG_DIR, catalog, APPS_DIR) + deal_readme(catalog_form_dir, output_file, CATALOG_FROM_README_DIR) + + log.info("transform catalog app form readme to html success") diff --git a/kdp_catalog_manager/domain/service/save_data_cache.py b/kdp_catalog_manager/domain/service/save_data_cache.py new file mode 100644 index 0000000..f6f34c9 --- /dev/null +++ b/kdp_catalog_manager/domain/service/save_data_cache.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os + +from kdp_catalog_manager.common.constants import METADATA_YAML, CATALOG_KEY, \ + CATALOG_FORM_KEY, APP_YAML, CATALOG_FROM_DATA_KEY +from kdp_catalog_manager.config.base_config import CACHE_EXPIRE +from kdp_catalog_manager.config.base_config import CATALOG_DIR, APPS_DIR +from kdp_catalog_manager.exceptions.exception import CacheOperatorError +from kdp_catalog_manager.modules.cache.cache import cache_instance +from kdp_catalog_manager.utils.log import log +from kdp_catalog_manager.utils.yamlutils import YAMLUtils + + +class SaveDataToCache(object): + def __init__(self): + self.cache = cache_instance + + +class SaveCatalogDataCache(SaveDataToCache): + def __init__(self): + super().__init__() + self.catalog_dir = CATALOG_DIR + + def get_catalog_data(self): + catalog_data = {} + for catalog in os.listdir(self.catalog_dir): + catalog_metadata_file = os.path.join( + CATALOG_DIR, catalog, METADATA_YAML) + if not os.path.exists(catalog_metadata_file): + continue + + catalog_metadata = YAMLUtils().load_all_yaml(catalog_metadata_file) + catalog_data[catalog] = catalog_metadata + rt = self.cache.set(CATALOG_KEY, catalog_data, CACHE_EXPIRE) + if rt: + log.info("initialization catalog data to cache successful") + else: + raise CacheOperatorError() + + +class SaveCatalogFormDataCache(SaveDataToCache): + def __init__(self): + super().__init__() + self.catalog_dir = CATALOG_DIR + self.apps = APPS_DIR + + def get_catalog_form_data(self): + catalog_form = {} + for catalog in os.listdir(self.catalog_dir): + catalog_form_dir = os.path.join( + self.catalog_dir, catalog, self.apps) + catalog_form_metadata_info = {} + for catalog_from_app in os.listdir(catalog_form_dir): + if not catalog_from_app.endswith(".app"): + continue + + # 存储元数据至缓存中 + catalog_form_metadata_file = os.path.join( + catalog_form_dir, catalog_from_app, METADATA_YAML) + if not os.path.exists(catalog_form_metadata_file): + continue + + catalog_form_metadata = YAMLUtils().load_all_yaml( + catalog_form_metadata_file) + + catalog_form_app_name = catalog_from_app.split(".")[0] + catalog_form_metadata_info[catalog_form_app_name] = ( + catalog_form_metadata) + + # 存储应用模板信息至缓存中 + catalog_form_data_file = os.path.join( + catalog_form_dir, catalog_from_app, APP_YAML + ) + if not os.path.exists(catalog_form_data_file): + continue + catalog_form_data = YAMLUtils().load_all_yaml( + catalog_form_data_file) + + self.cache.set( + CATALOG_FROM_DATA_KEY.format(catalog, catalog_form_app_name), + catalog_form_data, + CACHE_EXPIRE + ) + + catalog_form[catalog] = catalog_form_metadata_info + rt = self.cache.set(CATALOG_FORM_KEY, catalog_form, CACHE_EXPIRE) + if rt: + log.info("initialization catalog from data to cache successful") + else: + raise CacheOperatorError("save catalog form data to cache failed") diff --git a/kdp_catalog_manager/domain/service/test/__init__.py b/kdp_catalog_manager/domain/service/test/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/domain/service/test/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/domain/service/test/test_application.py b/kdp_catalog_manager/domain/service/test/test_application.py new file mode 100644 index 0000000..e69de29 diff --git a/kdp_catalog_manager/domain/service/test/test_catalog.py b/kdp_catalog_manager/domain/service/test/test_catalog.py new file mode 100644 index 0000000..445b6b5 --- /dev/null +++ b/kdp_catalog_manager/domain/service/test/test_catalog.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from unittest import TestCase + +from kdp_catalog_manager.common.constants import CATALOG_KEY +from kdp_catalog_manager.exceptions.exception import FileNotExistsError, \ + APIRequestedInvalidParamsError +from kdp_catalog_manager.modules.cache.cache import cache_instance +from ..catalog import CatalogController + + +class TestCatalogController(TestCase): + def setUp(self): + self.catalog_data = { + "mysql": { + "category": "系统/大数据开发工具", + "description": "mysql", + "i18n": { + "en": { + "category": "system.dataManagement", + "description": "mysql" + } + } + } + } + cache_instance.set(CATALOG_KEY, self.catalog_data) + + def test_get_catalog_data_is_None(self): + self.catalog_data = {"mysql": None} + cache_instance.set(CATALOG_KEY, self.catalog_data) + rt = CatalogController(catalog="mysql").get_catalog() + self.assertEqual(rt, {}) + + def test_get_catalog(self): + self.catalog_data = {"mysql": {}} + cache_instance.set(CATALOG_KEY, self.catalog_data) + rt = CatalogController(catalog="mysql").get_catalog() + self.assertEqual(rt, {}) + + def test_get_catalog_readme(self): + try: + CatalogController(catalog="mysql", lang="zhs").get_catalog_readme() + except FileNotExistsError: + self.assertEqual(True, True) + + def test_get_catalog_readme_catalog_is_None(self): + try: + CatalogController(lang="zhs").get_catalog_readme() + except APIRequestedInvalidParamsError: + self.assertEqual(True, True) diff --git a/kdp_catalog_manager/domain/service/test/test_catalog_form.py b/kdp_catalog_manager/domain/service/test/test_catalog_form.py new file mode 100644 index 0000000..8a7300b --- /dev/null +++ b/kdp_catalog_manager/domain/service/test/test_catalog_form.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from unittest import TestCase + +from kdp_catalog_manager.common.constants import CATALOG_FORM_KEY +from kdp_catalog_manager.exceptions.exception import FileNotExistsError, \ + APIRequestedInvalidParamsError +from kdp_catalog_manager.modules.cache.cache import cache_instance +from ..catalog_form import CatalogFormController, CacheNotExistsError, \ + CatalogFormMatchError + +TEST_DESC = "Mysql T" + + +class TestCatalogFormController(TestCase): + def setUp(self): + self.catalog_form_metadata = { + "mysql": { + "mysql": { + "version": "1.0.0", + "alias": "mysql", + "description": TEST_DESC, + "isGlobal": False, + "i18n": { + "en": { + "category": "system.dataManagement", + "description": "Mysql M" + } + } + } + } + } + + self.catalog_form_data = { + "apiVersion": "bdc.bdos.io/v1alpha1", + "kind": "Application" + } + + self.rt = { + "mysql": { + "mysql": { + "name": "mysql", + "version": "1.0.0", + "alias": "mysql", + "isGlobal": False, + "description": TEST_DESC, + "invisible": False, + "catalog": "mysql", + "dashboardUrl": [] + } + } + } + + def set_cache(self): + cache_instance.set(CATALOG_FORM_KEY, self.catalog_form_metadata) + + def test_get_catalog_form_metadata_from_cache_not_exists(self): + self.catalog_form_metadata = {} + self.set_cache() + try: + CatalogFormController().get_catalog_form_metadata_for_cache() + except CacheNotExistsError: + self.assertEqual(True, True) + + def test_get_catalog_form_metadata_from_cache(self): + self.set_cache() + rt = CatalogFormController().get_catalog_form_metadata_for_cache() + self.assertEqual(rt, self.catalog_form_metadata) + + def test_get_catalog_form_metadata(self): + self.set_cache() + rt = CatalogFormController().get_format_catalog_metadata() + self.assertEqual(rt, self.rt) + + def test_get_catalogs_forms(self): + self.set_cache() + rt = CatalogFormController( + catalog="mysql", app_form="mysql").get_catalogs_forms() + self.assertEqual(rt, [{ + "name": "mysql", "version": "1.0.0", "alias": 'mysql', + 'invisible': False, 'isGlobal': False, + 'description': TEST_DESC, "catalog": "mysql", 'dashboardUrl': []}]) + + def test_get_catalogs_forms_invisible(self): + self.set_cache() + rt = CatalogFormController( + catalog="mysql", app_form="mysql", invisible=True + ).get_catalogs_forms() + self.assertEqual(rt, []) + + def test_get_catalogs_form_catalog_not_found_from(self): + self.set_cache() + try: + CatalogFormController( + catalog="mysql", app_form="spark").get_catalogs_form() + except CatalogFormMatchError: + self.assertEqual(True, True) + + def test_get_catalogs_form_invisible(self): + self.set_cache() + cache_instance.set("mysql", self.catalog_form_metadata) + rt = CatalogFormController( + catalog="mysql", app_form="mysql", invisible=True + ).get_catalogs_form() + self.assertEqual(rt, {}) + cache_instance.delete("mysql") + + def test_get_catalogs_form(self): + self.set_cache() + cache_instance.set("mysql", self.catalog_form_metadata) + rt = CatalogFormController( + catalog="mysql", app_form="mysql").get_catalogs_form() + self.assertEqual(rt, { + 'name': 'mysql', 'version': '1.0.0', 'alias': 'mysql', + 'isGlobal': False, 'description': 'Mysql T', + 'invisible': False, "catalog": "mysql", 'dashboardUrl': []}) + cache_instance.delete("mysql") + + def test_get_catalog_form_data(self): + self.set_cache() + cache_instance.set("mysql-mysql-data", self.catalog_form_data) + rt = CatalogFormController( + catalog="mysql", app_form="mysql").get_catalog_form_data() + self.assertEqual(rt, self.catalog_form_data) + + def test_get_catalog_form_data_not_exists(self): + self.set_cache() + cache_instance.set("mysql-mysql-data", self.catalog_form_data) + rt = CatalogFormController( + catalog="mysql", app_form="mysql1").get_catalog_form_data() + self.assertEqual(rt, {}) + + def test_get_catalog_form_readme_not_exists(self): + try: + CatalogFormController( + catalog="mysql", app_form="mysql1").get_catalog_form_readme() + except FileNotExistsError: + self.assertEqual(True, True) + + def test_get_catalog_form_readme_not_catalog(self): + try: + CatalogFormController().get_catalog_form_readme() + except APIRequestedInvalidParamsError: + self.assertEqual(True, True) + + def test_get_form_info(self): + rt = CatalogFormController().get_form_info() + self.assertEqual(rt, {"mysql": "mysql"}) diff --git a/kdp_catalog_manager/exceptions/__init__.py b/kdp_catalog_manager/exceptions/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/exceptions/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/exceptions/err_map.json b/kdp_catalog_manager/exceptions/err_map.json new file mode 100644 index 0000000..51909f8 --- /dev/null +++ b/kdp_catalog_manager/exceptions/err_map.json @@ -0,0 +1,50 @@ +{ + "KdpCatalogManagerError": { + "code": 100000, + "error-name": "KdpCatalogManagerError", + "description": "", + "solution": "请查看日志获取更多错误详情", + "manual": "", + "app": "KDP-Catalog-Manager" + }, + "LoadJSONFileError": { + "code": 100001, + "error-name": "LoadJSONFileError", + "description": "读取的{name} 配置文件内容不是合法的JSON格式", + "solution": "检查文件内容", + "manual": "", + "app": "" + }, + "LangNotSupport":{ + "code": 300001, + "error-name": "LangNotSupport", + "description": "语言不支持", + "solution": "请传输正确语言参数", + "manual": "", + "app": "" + }, + "CatalogFormMatchError": { + "code": 300001, + "error-name": "CatalogFormMatchError", + "description": "catalog 与from 不匹配", + "solution": "检查查询参数是否正确", + "manual": "", + "app": "" + }, + "CacheOperatorError": { + "code": 100101, + "error-name": "CacheOperatorError", + "description": "缓存操作失败: {name}", + "solution": "检查应用缓存数据或重启应用", + "manual": "", + "app": "" + }, + "DataTypeError": { + "code": 100201, + "error-name": "DataTypeError", + "description": "缓存操作失败: {name}", + "solution": "检查应用缓存数据或重启应用", + "manual": "", + "app": "" + } +} \ No newline at end of file diff --git a/kdp_catalog_manager/exceptions/exception.py b/kdp_catalog_manager/exceptions/exception.py new file mode 100644 index 0000000..87747e7 --- /dev/null +++ b/kdp_catalog_manager/exceptions/exception.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +import traceback +import os +from kdp_catalog_manager.utils.log import logger + + +def get_exception(): + # 获取异常的跟踪信息 + exc_traceback = traceback.format_exc() + + # 提取文件名部分并打印跟踪信息 + basename_traceback = os.path.basename(exc_traceback) + logger.error(basename_traceback) + + +class KdpCatalogManagerError(Exception): + def __init__(self, error_cname=None): + self.error_name = self.__class__.__name__ + self.error_details = get_exception() + if not error_cname: + self.error_cname = "Kdp Catalog Manager Error" + else: + self.error_cname = "{}".format(error_cname) + super().__init__(self.error_cname) + + def __repr__(self): + return "'{}({})'".format(self.error_name, self.error_cname) + + +class LangNotSupport(KdpCatalogManagerError): + def __init__(self, name): + self.error_cname = "语言不支持:{}".format(name) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class DirectoryNotExistsError(KdpCatalogManagerError): + def __init__(self, file_path): + self.error_cname = "路径 {} 不存在".format(file_path) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class FileNotExistsError(KdpCatalogManagerError): + def __init__(self, file_path): + self.error_cname = "文件 {} 不存在".format(file_path) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class ReadFileError(KdpCatalogManagerError): + def __init__(self, file_path): + self.error_cname = "读取文件 {} 失败".format(file_path) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class HTTPRequestError(KdpCatalogManagerError): + def __init__(self, method, url, params=None, payload=None, msg=None): + __basic_error_cname = "请求method={} url={}失败 {},请检查该服务是否运行正常".format(method, url, msg) + self.error_cname = __basic_error_cname + if params: + self.error_cname = "{}(请求参数Params={})".format(__basic_error_cname, params) + if payload: + self.error_cname = "{}(请求参数Body={})".format(__basic_error_cname, payload) + if params and payload: + self.error_cname = "{}(请求参数Params={},Body={})".format(__basic_error_cname, params, payload) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class APIRequestedURLNotFoundError(KdpCatalogManagerError): + def __init__(self): + self.error_cname = "API 请求URL不存在" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class APIRequestedInvalidParamsError(KdpCatalogManagerError): + def __init__(self, error_msg=""): + self.error_cname = "API 请求参数不正确:{}".format(error_msg) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class WriteFileError(KdpCatalogManagerError): + def __init__(self, file_path): + self.error_cname = "保存(写)文件 {} 失败".format(file_path) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class FileOpsForbidden(KdpCatalogManagerError): + def __init__(self, file_path): + self.error_cname = "文件路径 {} 不合法".format(file_path) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class CreateSoftLinkError(KdpCatalogManagerError): + def __init__(self, src, dst): + self.error_cname = "创建软链 {}->{} 失败".format(src, dst) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class MaxTryError(KdpCatalogManagerError): + """超过最大重试次数""" + + +class LoadYamlError(KdpCatalogManagerError): + def __init__(self, name=""): + self.error_cname = "获取YAML失败:{}".format(name) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class CopyFileError(KdpCatalogManagerError): + def __init__(self, error_msg): + self.error_cname = "拷贝文件失败 {}".format(error_msg) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class DirNotExistsError(KdpCatalogManagerError): + def __init__(self, dir_path): + self.error_cname = "目录 {} 不存在".format(dir_path) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class CopyDirError(KdpCatalogManagerError): + def __init__(self, error_msg): + self.error_cname = "拷贝目录失败 {}".format(error_msg) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class RemoveDirError(KdpCatalogManagerError): + def __init__(self, error_msg): + self.error_cname = "删除目录失败 {}".format(error_msg) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class GetAppInfoError(KdpCatalogManagerError): + def __init__(self, error_msg=""): + self.error_cname = "获取应用描述信息失败 {}".format(error_msg) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class ConvertEngineDataError(KdpCatalogManagerError): + def __init__(self, name=""): + self.error_cname = "[实例化应用]转换配置失败 {}".format(name) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class JSONSchemaError(KdpCatalogManagerError): + def __init__(self, name=""): + self.error_cname = "JSONSchemaError:{}".format(name) + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class CacheNotSupportError(KdpCatalogManagerError): + def __init__(self, name=""): + self.error_cname = f"缓存类型{name}不支持" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class CacheOperatorError(KdpCatalogManagerError): + def __init__(self, name=""): + self.error_cname = f"缓存操作失败: {name}" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class CacheNotExistsError(KdpCatalogManagerError): + def __init__(self, name=""): + self.error_cname = f"缓存不存在: {name}" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class LoadJSONFileError(KdpCatalogManagerError): + def __init__(self, file_path): + self.error_cname = f"读取的{file_path} 配置文件内容不是合法的JSON格式" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class DataNoneError(KdpCatalogManagerError): + def __init__(self, file_path): + self.error_cname = f"内容为空:{file_path}" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class CatalogFormMatchError(KdpCatalogManagerError): + def __init__(self, error_msg): + self.error_cname = f"应用目录与应用模板不匹配: {error_msg}" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class DataTypeError(KdpCatalogManagerError): + def __init__(self, error_msg): + self.error_cname = f"数据类型异常: {error_msg}" + KdpCatalogManagerError.__init__(self, self.error_cname) + + +class GetDataError(KdpCatalogManagerError): + def __init__(self, error_msg): + self.error_cname = f"获取数据类型: {error_msg}" + KdpCatalogManagerError.__init__(self, self.error_cname) \ No newline at end of file diff --git a/kdp_catalog_manager/main.py b/kdp_catalog_manager/main.py new file mode 100644 index 0000000..1b31910 --- /dev/null +++ b/kdp_catalog_manager/main.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os +from contextlib import asynccontextmanager + +import fastapi.openapi.utils as fu +from fastapi import FastAPI, status, Request, HTTPException +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse + +from kdp_catalog_manager.api.api_v1.api import api_router +from kdp_catalog_manager.domain.service.markdown_transform_html import \ + catalog_readme, catalog_form_readme +from kdp_catalog_manager.domain.service.save_data_cache import \ + SaveCatalogDataCache, SaveCatalogFormDataCache +from kdp_catalog_manager.utils.format_return import FormatReturn +from kdp_catalog_manager.utils.log import log +from prometheus_fastapi_instrumentator import Instrumentator + + +@asynccontextmanager +async def lifespan(app: FastAPI): + print("prestart runing ......") + instrumentator.expose(app) + yield + print("running before closing ......") + + +app = FastAPI( + title="KDP Catalog Manager", + version="1.0.0", + description="应用目录管理", + license_info={ + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, + openapi_url="/api/v1/openapi.json", + lifespan=lifespan +) + +# 添加监控指标 +instrumentator = Instrumentator().instrument(app) + +# 添加路由前缀 +app.include_router(api_router, prefix="/api/v1") + + +@app.get("/") +async def index(): + return {"ping": "pong"} + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + error_info = FormatReturn().format_error_info( + "RequestValidationError", + exc.errors(), + error_msg=exc.errors() + ) + rtn = FormatReturn().format_return_json( + {}, status=1, msg=exc.errors(), error_info=error_info) + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=jsonable_encoder( + rtn + ) + ) + + +@app.exception_handler(404) +def not_found_exception_handler(request: Request, exc: HTTPException): + return JSONResponse( + status_code=404, + content=FormatReturn().format_return_json( + data=exc.detail, + status=1, + msg=f"{request.query_params} {exc.detail}" + ) + ) + + +fu.validation_error_response_definition = { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "status": {"title": "err status", "type": "int", "default": 1}, + "message": {"title": "Message", "type": "string"}, + "data": {"title": "data Message", "type": "object"}, + "error": {"title": "err Message", "type": "object"} + } +} + +if not os.environ.get("Test"): + SaveCatalogDataCache().get_catalog_data() + SaveCatalogFormDataCache().get_catalog_form_data() + + catalog_readme() + log.info("initialization catalog readme transform to html successful") + catalog_form_readme() + log.info("initialization catalog form readme transform to html successful") + + +if __name__ == '__main__': + import uvicorn + uvicorn.run( + "main:app", host="0.0.0.0", port=8888, + ) diff --git a/kdp_catalog_manager/modules/__init__.py b/kdp_catalog_manager/modules/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/modules/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/modules/cache/__init__.py b/kdp_catalog_manager/modules/cache/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/modules/cache/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/modules/cache/cache.py b/kdp_catalog_manager/modules/cache/cache.py new file mode 100644 index 0000000..9ffb635 --- /dev/null +++ b/kdp_catalog_manager/modules/cache/cache.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from cachelib import SimpleCache + + +cache_instance = SimpleCache() diff --git a/kdp_catalog_manager/modules/http_requests/__init__.py b/kdp_catalog_manager/modules/http_requests/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/modules/http_requests/application/__init__.py b/kdp_catalog_manager/modules/http_requests/application/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/application/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/modules/http_requests/application/oam_application.py b/kdp_catalog_manager/modules/http_requests/application/oam_application.py new file mode 100644 index 0000000..46a9c26 --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/application/oam_application.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from kdp_catalog_manager.modules.http_requests.base_requests import BaseRequests +from kdp_catalog_manager.common.constants import HTTP_HEADER, RESPONSE_DATA +from kdp_catalog_manager.config.base_config import OAM_BASE_URL, HTTP_TIME_OUT, \ + HTTP_MAX_RETRIES + + +class OamApplication(BaseRequests): + def __init__(self, timeout=HTTP_TIME_OUT, max_retries=HTTP_MAX_RETRIES): + super().__init__(timeout=timeout, max_retries=max_retries) + self.headers = HTTP_HEADER + + def list_all(self, params=None): + _url = "{}/api/v1/applications".format(OAM_BASE_URL) + _response = self.http_request( + "GET", _url, self.headers, params=params + ) + return _response.get(RESPONSE_DATA) diff --git a/kdp_catalog_manager/modules/http_requests/application/test/__init__.py b/kdp_catalog_manager/modules/http_requests/application/test/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/application/test/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/modules/http_requests/application/test/test_oam_application.py b/kdp_catalog_manager/modules/http_requests/application/test/test_oam_application.py new file mode 100644 index 0000000..3db9219 --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/application/test/test_oam_application.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from unittest import TestCase, mock +from ..oam_application import OamApplication +from kdp_catalog_manager.exceptions.exception import HTTPRequestError + + +class TestOamApplication(TestCase): + def setUp(self): + self.application = OamApplication(1, 2) + + def test_list_all_normal(self): + with self.assertRaises(HTTPRequestError): + self.application.list_all() + + def test_list_all_success(self): + self.rt = { + "status": 0, + "message": "ok", + "data": [ + ] + } + self.application.http_request = mock.Mock(return_value=self.rt) + response = self.application.list_all() + self.assertEqual(response, []) diff --git a/kdp_catalog_manager/modules/http_requests/base_requests.py b/kdp_catalog_manager/modules/http_requests/base_requests.py new file mode 100644 index 0000000..d84c20a --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/base_requests.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import traceback + +import httpx + +from kdp_catalog_manager.common.constants import RESPONSE_NORMAL_CODE, \ + RESPONSE_NOT_FOUND_CODE +from kdp_catalog_manager.config.base_config import HTTP_TIME_OUT, \ + HTTP_MAX_RETRIES +from kdp_catalog_manager.exceptions.exception import HTTPRequestError, \ + APIRequestedURLNotFoundError +from kdp_catalog_manager.utils.log import log + + +class BaseRequests(object): + def __init__(self, timeout=HTTP_TIME_OUT, max_retries=HTTP_MAX_RETRIES): + self.timeout = timeout + self.max_retries = max_retries + + def http_request( + self, + method, + url, + headers, + data=None, + params=None + ): + try: + log.info("request url: [{}]{}".format(method, url)) + log.info("request body: {}".format(data)) + log.info("request params: {}".format(params)) + transport = httpx.HTTPTransport(retries=self.max_retries) + with httpx.Client(transport=transport) as client: + response = client.request( + method, + url, + headers=headers, + params=params, + timeout=self.timeout, + json=data + ) + if response.status_code == RESPONSE_NORMAL_CODE: + return response.json() + if response.status_code == RESPONSE_NOT_FOUND_CODE and \ + response.text == "404: Page Not Found": + raise APIRequestedURLNotFoundError() + else: + raise HTTPRequestError( + method, url, + msg=f"{response.status_code}, response.json()") + except Exception as e: + print(traceback.format_exc()) + raise HTTPRequestError(method, url, msg=str(e)) diff --git a/kdp_catalog_manager/modules/http_requests/test/__init__.py b/kdp_catalog_manager/modules/http_requests/test/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/test/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/modules/http_requests/test/test_base_requests.py b/kdp_catalog_manager/modules/http_requests/test/test_base_requests.py new file mode 100644 index 0000000..56de120 --- /dev/null +++ b/kdp_catalog_manager/modules/http_requests/test/test_base_requests.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from unittest import TestCase +from unittest import mock + +from kdp_catalog_manager.exceptions.exception import HTTPRequestError +from kdp_catalog_manager.modules.http_requests.base_requests import BaseRequests, RESPONSE_NOT_FOUND_CODE, APIRequestedURLNotFoundError + + +class TestHttpRequest(TestCase): + def setUp(self): + self.maxDiff = None + self.header = {"Content-Type": "application/json; charset=UTF-8"} + self.base_requests = BaseRequests(timeout=1, max_retries=1) + + def test_http_request(self): + rt = True + try: + self.base_requests.http_request( + "GET", "http://127.0.0.1", + self.header + ) + except HTTPRequestError: + self.assertEqual(rt, True) + + def test_http_request1(self): + rt = self.base_requests.http_request( + "GET", "https://wenku.baidu.com/message/getnotice", + self.header) + self.assertEqual(rt.get("status").get("code"), 0) + self.assertEqual(rt, { + "status": {"code": 0, "msg": "get notice success"}, + "data": {"system": [], "diy": [], "advert": [], + "errstr": "get notice success"} + }) + + @mock.patch('httpx.Client') + def test_http_request_404(self, mock_client): + mock_response = mock.MagicMock() + mock_response.status_code = 404 + mock_response.text = "404: Page Not Found" + mock_client.return_value.__enter__.return_value.request.return_value = mock_response + with self.assertRaises(HTTPRequestError): + self.base_requests.http_request( + "GET", "http://127.0.0.1:8002/api/v1/applicationss", + self.header) + + @mock.patch('httpx.Client') + def test_http_request_500(self, mock_client): + mock_response = mock.MagicMock() + mock_response.status_code = 500 + with self.assertRaises(HTTPRequestError): + self.base_requests.http_request( + "GET", "http://127.0.0.1:8002/api/v1/applicationss", + self.header) diff --git a/kdp_catalog_manager/setup.py b/kdp_catalog_manager/setup.py new file mode 100644 index 0000000..e47f412 --- /dev/null +++ b/kdp_catalog_manager/setup.py @@ -0,0 +1,49 @@ +import os +import re +import sys +from Cython.Build import cythonize +from Cython.Compiler import Options +from distutils.core import Extension, setup + +__ABS_DIR_PATH = os.path.abspath('') +exclude_so = ["__init__.py", "setup.py", "main.py", "gunicorn.py"] +sources = ['./'] + +extensions = [] +sources_files = [] +for source in sources: + for dir_path, folder_names, filenames in os.walk(source): + for filename in filter(lambda x: re.match(r'.*[.]py$', x), filenames): + print(filename) + file_path = os.path.join(dir_path, filename) + print(file_path) + if filename not in exclude_so: + sources_files.append(file_path) + if filename not in exclude_so: + print("debug point ", file_path[:-3].replace('/', '.')[2:]) + extensions.append( + Extension(file_path[:-3].replace('/', '.')[2:], [file_path], extra_compile_args=["-Os", "-g0"], + extra_link_args=["-Wl,--strip-all"])) + +print(sources_files) +Options.docstrings = False +compiler_directives = {'optimize.unpack_method_calls': False, 'always_allow_keywords': True} + +try: + setup( + ext_modules=cythonize( + extensions, + exclude=None, + nthreads=20, + quiet=True, + language_level=3, + compiler_directives=compiler_directives + ) + ) + print("Start remove source files...") + for sf in sources_files: + os.remove(sf) + os.remove('{}.c'.format(sf[:-3])) +except Exception as ex: + print("error! ", str(ex)) + sys.exit(1) diff --git a/kdp_catalog_manager/test_main.py b/kdp_catalog_manager/test_main.py new file mode 100644 index 0000000..96f3a47 --- /dev/null +++ b/kdp_catalog_manager/test_main.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from .main import app +from fastapi.testclient import TestClient + +client = TestClient(app) + + +def test_read_main(): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"ping": "pong"} diff --git a/kdp_catalog_manager/utils/__init__.py b/kdp_catalog_manager/utils/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/utils/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/utils/dictutils.py b/kdp_catalog_manager/utils/dictutils.py new file mode 100644 index 0000000..c934c48 --- /dev/null +++ b/kdp_catalog_manager/utils/dictutils.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from functools import reduce + + +class DictUtils(object): + + @staticmethod + def get_items(obj, items, default=None): + """递归获取数据 + + :param obj: + :param default: + :param items: 键列表:['foo', 'bar'],或者用 "." 连接的键路径: ".foo.bar" 或 "foo.bar" + """ + if isinstance(items, str): + items = items.strip(".").split(".") + try: + return reduce(lambda x, i: x[i], items, obj) + except (IndexError, KeyError, TypeError): + return default diff --git a/kdp_catalog_manager/utils/fileutils.py b/kdp_catalog_manager/utils/fileutils.py new file mode 100644 index 0000000..a19bfe0 --- /dev/null +++ b/kdp_catalog_manager/utils/fileutils.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os + +from kdp_catalog_manager.exceptions.exception import FileNotExistsError, \ + ReadFileError, WriteFileError +from kdp_catalog_manager.utils.log import log + + +class FileUtils(object): + + @staticmethod + def check_file_exists(file, log_err=True): + if not os.path.lexists(file): + if log_err: + log.debug("File {} not exists!".format(file)) + return False + else: + return True + + def read_file(self, file): + if self.check_file_exists(file): + try: + with open(file) as f: + rtn = f.read() + return rtn + except IOError as e: + log.exception(e) + raise ReadFileError(file) + else: + log.warning("File {} not exists".format(file)) + raise FileNotExistsError(file) + + @staticmethod + def write_file(content, target_file): + try: + with open(target_file, 'w') as f: + f.write(content) + except Exception as err: + log.error(str(err)) + raise WriteFileError(target_file) diff --git a/kdp_catalog_manager/utils/format_return.py b/kdp_catalog_manager/utils/format_return.py new file mode 100644 index 0000000..4d39c5d --- /dev/null +++ b/kdp_catalog_manager/utils/format_return.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import json +from kdp_catalog_manager.utils.log import log + + +class FormatReturn(object): + + @staticmethod + def format_return_json(data, status=0, msg="", error_info={}): + if error_info: + status = 1 + msg = "BdosException occurred" if msg == "" else msg + data = None if data is None else data + + rtn = {'status': status, 'data': data, + 'message': msg, "error": error_info} + return rtn + + @staticmethod + def format_error_info( + error_name, raw_exception, app_name=None, func_args=None, + error_msg=None, exception_type="error" + ): + log.error(f"{error_name}: {error_msg}") + with open("kdp_catalog_manager/exceptions/err_map.json") as fp: + errors_map_list = json.load(fp) + error_info = { + "type": exception_type, + "exception": raw_exception, + "args": [] if func_args is None else func_args + } + error_data_info = errors_map_list.get( + error_name, + errors_map_list.get("KdpCatalogManagerError") + ) + error_info["info"] = error_data_info + if error_msg: + error_info["info"]['description'] = error_msg + if app_name: + error_info["app"] = app_name + else: + error_info["app"] = error_data_info["app"] + del error_data_info['app'] + + return error_info diff --git a/kdp_catalog_manager/utils/log.py b/kdp_catalog_manager/utils/log.py new file mode 100644 index 0000000..a8db13b --- /dev/null +++ b/kdp_catalog_manager/utils/log.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os +import logging +from concurrent_log_handler import ConcurrentRotatingFileHandler +from kdp_catalog_manager.config import log_config + + +# 日志记录器 +logger = logging.getLogger() +# 设置日志级别 +logger.setLevel(log_config.LOG_LEVEL) + +# 设置日志格式 +formatter = logging.Formatter( + "[%(asctime)s] [%(levelname)s] [PID:%(process)d][ThreadID:%(thread)d-%(threadName)s] %(module)s %(funcName)s line:%(lineno)d %(message)s" +) + +to_console = logging.StreamHandler() +to_console.setFormatter(formatter) + + +to_file = ConcurrentRotatingFileHandler( + os.path.join(log_config.LOG_DIR, log_config.LOG_FILE), + mode="a", + maxBytes=log_config.LOG_FILE_MAX_BYTES, + backupCount=log_config.LOG_FILE_BACKUP_COUNT +) +to_file.setFormatter(formatter) + +# 将日志输出至屏幕 +logger.addHandler(to_console) +# 将日志输出至文件 +logger.addHandler(to_file) + +log = logger + + +if not os.path.exists(log_config.LOG_DIR): + os.makedirs(log_config.LOG_DIR) diff --git a/kdp_catalog_manager/utils/markdown_to_html/__init__.py b/kdp_catalog_manager/utils/markdown_to_html/__init__.py new file mode 100644 index 0000000..ff6ea1e --- /dev/null +++ b/kdp_catalog_manager/utils/markdown_to_html/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- diff --git a/kdp_catalog_manager/utils/markdown_to_html/markdownToHtml.py b/kdp_catalog_manager/utils/markdown_to_html/markdownToHtml.py new file mode 100644 index 0000000..b4f86a4 --- /dev/null +++ b/kdp_catalog_manager/utils/markdown_to_html/markdownToHtml.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +import os + +import markdown + +from kdp_catalog_manager.config.base_config import EXTEND_EXTENSION +from kdp_catalog_manager.utils.fileutils import FileUtils + +MD_HTML_CSS = "kdp_catalog_manager/utils/markdown_to_html/md_html.css" + + +class HtmlController(object): + def __init__(self, css_file=MD_HTML_CSS): + self.css_file = css_file + self.html_css = os.path.join(os.getcwd(), self.css_file) + + def get_css(self): + return FileUtils().read_file(self.html_css) + + def create(self, data, file_name: str): + css_data = self.get_css() + html_data = css_data + data + FileUtils().write_file(html_data, file_name) + + +class MarkdownToHtlm(HtmlController): + def __init__(self): + super().__init__() + self.extensions = [ + 'extra', + 'tables', + 'codehilite' + ] + self.extend_extensions() + + def extend_extensions(self): + if EXTEND_EXTENSION: + custom_extensions = EXTEND_EXTENSION.split(",") + self.extensions.extend(custom_extensions) + + def mutate_markdown_data(self, input_file): + try: + markdown_data = FileUtils().read_file(input_file) + html_data = markdown.markdown( + markdown_data, + extensions=self.extensions + ) + return html_data + except Exception: + return None + + def markdown_to_html(self, input_file, output_file, output_dir=None): + markdown_html_data = self.mutate_markdown_data(input_file) + if output_dir is not None: + if not os.path.exists(output_dir): + os.makedirs(output_dir) + output_file = os.path.join(output_dir, output_file) + self.create(markdown_html_data, output_file) diff --git a/kdp_catalog_manager/utils/markdown_to_html/md_html.css b/kdp_catalog_manager/utils/markdown_to_html/md_html.css new file mode 100644 index 0000000..1a66764 --- /dev/null +++ b/kdp_catalog_manager/utils/markdown_to_html/md_html.css @@ -0,0 +1,259 @@ + + \ No newline at end of file diff --git a/kdp_catalog_manager/utils/markdown_to_html/test_markdownToHtml.py b/kdp_catalog_manager/utils/markdown_to_html/test_markdownToHtml.py new file mode 100644 index 0000000..278d517 --- /dev/null +++ b/kdp_catalog_manager/utils/markdown_to_html/test_markdownToHtml.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os.path +from unittest import TestCase + +from .markdownToHtml import HtmlController, MarkdownToHtlm + + +class TestHtmlController(TestCase): + def setUp(self): + + self.css_file = HtmlController().css_file + with open(self.css_file, "r") as f: + self.css_data = f.read() + + def test_get_css(self): + rt = HtmlController().get_css() + self.assertEqual(rt, self.css_data) + + def test_create(self): + output_file = "test.html" + HtmlController().create("test", output_file) + if os.path.exists(output_file): + self.assertEqual(True, True) + os.remove("test.html") + + +class TestMarkdownToHtlm(TestCase): + def setUp(self): + self.markdown_data = "# desc" + self.markdown_file = "test.md" + with open(self.markdown_file, "w") as f: + f.write(self.markdown_data) + + def test_mutate_markdown_data(self): + rt = MarkdownToHtlm().mutate_markdown_data(self.markdown_file) + self.assertEqual(rt, '

desc

') + + def tearDown(self): + os.remove(self.markdown_file) diff --git a/kdp_catalog_manager/utils/test_dictutils.py b/kdp_catalog_manager/utils/test_dictutils.py new file mode 100644 index 0000000..2dc3870 --- /dev/null +++ b/kdp_catalog_manager/utils/test_dictutils.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from unittest import TestCase +from .dictutils import DictUtils + + +class TestDicuUtils(TestCase): + def setUp(self): + self.raw = {"aa": 123, "bb": "123"} + + def test_get_items(self): + rt = DictUtils().get_items(self.raw, ["aa"]) + self.assertEqual(rt, 123) + + def test_get_items_not_found(self): + rt = DictUtils().get_items(self.raw, ["ab"]) + self.assertEqual(rt, None) + + def test_get_items1(self): + rt = DictUtils().get_items(self.raw, ".aa") + self.assertEqual(rt, 123) + + def test_get_items1_not_found(self): + rt = DictUtils().get_items(self.raw, ".ab") + self.assertEqual(rt, None) + + def test_get_items2(self): + rt = DictUtils().get_items(self.raw, "aa") + self.assertEqual(rt, 123) + + def test_get_items2_not_found(self): + rt = DictUtils().get_items(self.raw, "ab") + self.assertEqual(rt, None) + + def test_get_items3(self): + rt = DictUtils().get_items(self.raw, "bb") + self.assertEqual(rt, "123") diff --git a/kdp_catalog_manager/utils/test_fileutils.py b/kdp_catalog_manager/utils/test_fileutils.py new file mode 100644 index 0000000..ede2356 --- /dev/null +++ b/kdp_catalog_manager/utils/test_fileutils.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os +from unittest import TestCase +from unittest.mock import patch, mock_open + +from kdp_catalog_manager.exceptions.exception import FileNotExistsError, \ + ReadFileError, WriteFileError +from .fileutils import FileUtils + + +class TestFileUtils(TestCase): + def setUp(self): + self.file = "test.log" + with open(self.file, "a") as f: + f.write("test") + + def test_check_file_exists(self): + # 测试文件是否存在的情况 + with patch('os.path.lexists') as mock_lexists: + mock_lexists.return_value = True + self.assertTrue(FileUtils.check_file_exists(self.file)) + + # 测试文件不存在的情况 + with patch('os.path.lexists') as mock_lexists: + mock_lexists.return_value = False + self.assertFalse(FileUtils.check_file_exists('a.log')) + + @patch('builtins.open', new_callable=mock_open, read_data='test') + def test_read_file(self, mock_open): + # 测试文件已存在 + rt = FileUtils().read_file(self.file) + self.assertEqual(rt, "test") + + # 测试读取不存在的文件的情况 + with self.assertRaises(FileNotExistsError): + FileUtils().read_file("a.log") + + # 测试读取文件时发生IOError的情况 + with patch('builtins.open', side_effect=IOError): + with self.assertRaises(ReadFileError): + FileUtils().read_file(self.file) + + @patch('builtins.open', new_callable=mock_open) + def test_write_file(self, mock_open): + FileUtils().write_file("test1", self.file) + + # 测试写文件时发生IOError的情况 + with patch('builtins.open', side_effect=IOError): + with self.assertRaises(WriteFileError): + FileUtils().write_file("test1", self.file) + + def tearDown(self): + os.remove(self.file) diff --git a/kdp_catalog_manager/utils/test_yamlutils.py b/kdp_catalog_manager/utils/test_yamlutils.py new file mode 100644 index 0000000..28e2c8c --- /dev/null +++ b/kdp_catalog_manager/utils/test_yamlutils.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +import os +from unittest import TestCase +from unittest.mock import patch, mock_open + +import yaml +from yaml import SafeLoader + +from .yamlutils import YAMLUtils, FileNotExistsError, LoadYamlError + + +class TestYamlUtils(TestCase): + def setUp(self): + self.data = { + "name": "test", + "city": { + "en": "china" + } + } + self.yaml_file = "test.yaml" + self.yaml_data = yaml.dump(self.data) + with open(self.yaml_file, "w") as f: + f.write(yaml.dump(self.data)) + + def test_load_all_yaml_many(self): + self.yaml_data = """--- +name: test + +--- +name: test1""" + with open(self.yaml_file, "w") as f: + f.write(self.yaml_data) + rt = YAMLUtils().load_all_yaml(self.yaml_file) + self.assertEqual(rt, [{"name": "test"}, {"name": "test1"}]) + + def test_load_all_yaml(self): + with open(self.yaml_file, "w") as f: + f.write(self.yaml_data) + rt = YAMLUtils().load_all_yaml(self.yaml_file) + self.assertEqual(rt, self.data) + + @patch('os.path.exists', return_value=False) + def test_load_all_yaml_non_existing_file(self, mock_exists): + with self.assertRaises(FileNotExistsError): + YAMLUtils.load_all_yaml('t.yaml') + + @patch('builtins.open', side_effect=Exception) + @patch('os.path.exists', return_value=True) + def test_load_all_yaml_exception_handling(self, mock_exists, mock_open): + with self.assertRaises(LoadYamlError): + YAMLUtils.load_all_yaml(self.yaml_file) + + def test_load_yaml(self): + rt = YAMLUtils().load_yaml(self.yaml_file) + self.assertEqual(rt, self.data) + + def test_load_yaml_non_existing_file(self): + with self.assertRaises(FileNotExistsError): + YAMLUtils().load_yaml("tt.yaml") + + def test_load_yaml_err(self): + err_log_file = "err.log" + self.yaml_data = """name: -""" + with open(err_log_file, "w") as f: + f.write(self.yaml_data) + with self.assertRaises(LoadYamlError): + YAMLUtils().load_yaml(err_log_file) + os.remove(err_log_file) + + @patch('yaml.dump_all') + def test_json_to_yaml(self, mock_dump_all): + YAMLUtils().json_to_yaml(self.data) + mock_dump_all.assert_called_once_with( + self.data, default_flow_style=False) + + @patch('yaml.dump') + def test_json_to_yaml_single(self, mock_dump): + YAMLUtils().json_to_yaml(self.data, dump_all=False) + mock_dump.assert_called_once_with(self.data, default_flow_style=False) + + @patch('yaml.load') + def test_yaml_to_json_single(self, mock_load_all): + YAMLUtils().single_yaml_to_json(self.yaml_data) + mock_load_all.assert_called_once_with( + self.yaml_data, + SafeLoader + ) + + def tearDown(self): + os.remove(self.yaml_file) diff --git a/kdp_catalog_manager/utils/yamlutils.py b/kdp_catalog_manager/utils/yamlutils.py new file mode 100644 index 0000000..2e80792 --- /dev/null +++ b/kdp_catalog_manager/utils/yamlutils.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# @Time : 2021/1/19 10:07 上午 +import os + +import yaml + +from kdp_catalog_manager.exceptions.exception import LoadYamlError, \ + FileNotExistsError +from kdp_catalog_manager.utils.log import log + + +class YAMLUtils(object): + @staticmethod + def load_all_yaml(yaml_file_path): + if not os.path.exists(yaml_file_path): + raise FileNotExistsError(f"The source yaml file {yaml_file_path} not exists") + try: + data = [] + with open(yaml_file_path, 'r') as configfile: + yaml_file_content = yaml.safe_load_all(configfile) + for value in yaml_file_content: + data.append(value) + if len(data) == 1: + return data[0] + # yaml_file_content's type is dict + return data + except Exception as e: + log.exception(str(e), exc_info=True) + raise LoadYamlError(e) + + @staticmethod + def load_yaml(yaml_file_path): + if not os.path.exists(yaml_file_path): + raise FileNotExistsError(f"The source yaml file {yaml_file_path} not exists") + try: + with open(yaml_file_path, 'r') as configfile: + yaml_file_content = yaml.safe_load(configfile) + # yaml_file_content's type is dict + data = yaml_file_content + return data + except Exception as e: + log.exception(str(e), exc_info=True) + raise LoadYamlError(e) + + @staticmethod + def json_to_yaml(json_data, dump_all=True): + try: + if not dump_all: + return yaml.dump( + json_data, + default_flow_style=False + ) + return yaml.dump_all( + json_data, + default_flow_style=False + ) + except Exception as e: + log.exception(str(e), exc_info=True) + raise + + @staticmethod + def single_yaml_to_json(yaml_data): + try: + _py_object_data = yaml.safe_load(yaml_data) + return _py_object_data + except Exception as e: + log.exception(str(e), exc_info=True) + raise diff --git a/makefiles/build-image.mk b/makefiles/build-image.mk new file mode 100644 index 0000000..0920745 --- /dev/null +++ b/makefiles/build-image.mk @@ -0,0 +1,23 @@ +# Docker image info +IMG ?= linktimecloud/kdp-catalog-manager:$(VERSION) +IMG_REGISTRY ?= "" + +.PHONY: set-build-info +set-build-info: + git log -1 --pretty=format:"{ \"GitVersion\": {%n \"branch\": \"$(GIT_BRANCH)\",%n \"commit\": \"%H\",%n \"author\": \"%an <%ae>\",%n \"date\": \"%ai\",%n \"message\": \"$(GIT_COMMIT_MESSAGE)\",%n \"tag\": \"$(GIT_TAG)\"%n},\"Version\": \"$(VERSION)\"%n,\"BuildDate\": \"$(BUILD_DATE)\"%n}" > kdp_catalog_manager/git.json + +.PHONY: docker-build +docker-build: docker-build-image + @echo "Docker build complete." + +.PHONY: docker-build-image +docker-build-image: + docker build -t $(IMG_REGISTRY)/$(IMG) -f Dockerfile . + +.PHONY: docker-push +docker-push: docker-push-image + @echo "Docker push complete." + +.PHONY: docker-push-image +docker-push-image: + docker push $(IMG_REGISTRY)/$(IMG) diff --git a/makefiles/const.mk b/makefiles/const.mk new file mode 100644 index 0000000..3a63895 --- /dev/null +++ b/makefiles/const.mk @@ -0,0 +1,25 @@ +# Setting SHELL to bash allows bash commands to be executed by recipes. +SHELL = /usr/bin/env bash -o pipefail +.SHELL_FLAGS = -ec + + +# Git Repo info +BUILD_DATE := $(shell date -u +'%Y-%m-%dT%H:%M:%SZ') + +GIT_COMMIT ?= git-$(shell git rev-parse --short HEAD) +GIT_COMMIT_LONG ?= $(shell git rev-parse HEAD) +GIT_COMMIT_MESSAGE := $(shell git log -1 --pretty=format:"%s" | sed 's/"/\\"/g') +GIT_REMOTE := origin +GIT_BRANCH := $(shell git rev-parse --symbolic-full-name --verify --quiet --abbrev-ref HEAD) +GIT_TAG := $(shell git describe --exact-match --tags --abbrev=0 2> /dev/null || echo untagged) +GIT_TREE_STATE := $(shell if [[ -z "`git status --porcelain`" ]]; then echo "clean" ; else echo "dirty"; fi) +RELEASE_TAG := $(shell if [[ "$(GIT_TAG)" =~ ^[.\|v][0-9]+\.[0-9]+\.[0-9]+.*$$ ]]; then echo "true"; else echo "false"; fi) + +VERSION := latest +ifeq ($(RELEASE_TAG),true) +VERSION := $(GIT_TAG) +endif + +$(info GIT_COMMIT=$(GIT_COMMIT) GIT_COMMIT_MESSAGE=$(GIT_COMMIT_MESSAGE) GIT_BRANCH=$(GIT_BRANCH) GIT_TAG=$(GIT_TAG) GIT_TREE_STATE=$(GIT_TREE_STATE) RELEASE_TAG=$(RELEASE_TAG) VERSION=$(VERSION)) + +ARTIFACTS_SERVER ?= "" \ No newline at end of file