Hi, this is Milo, a blogger who would like to share his test development techniques, interview tips and growing experiences with you!

Welcome everyone to pay attention to my public number: test development pit goods.

review

In the previous section we wrote the environment management function, this chapter we continue to improve the global variable function.

Global variables?

Global variable, actually I think it’s better to call it global configuration. What I think of as a global variable is data that doesn’t change very much, whereas a variable that appears in a use case, I think of as a temporary data that is not stored permanently.

So when do you use these variables? For example, some of our common test addresses, IP or URL, if the use case does not remove the content out, once the address changes, there will be two problems.

  • We need to modify the use case
  • The cost of modification is large

Some students might say, well, why don’t I just go to the database and update it? Yes, you can do that. But who can guarantee that the update will not make mistakes, and who can guarantee that the update will be timely every time? So, if I can make uniform changes on the global variables page, wouldn’t that be more effective? That’s what I’m talking about, it’s really more of a global configuration.

Design ideas

In fact, we provide a global configuration, similar to Redis, is a key-value form. Users only need to enter the key to map to the corresponding value.

If a value can hold a variety of objects, such as int, STR, float, etc. Therefore, we need to provide some assistance at this time. We introduce a key_type field to realize the function of multiple data types of one field together with value.

But I don’t want it to be too complicated, because we’re lazy people. In fact, we can define a few types:

  • string
  • json
  • yaml

Why only 3 types? Json.loads () is not required to fetch data in case of string, json.loads() is used to convert data in case of JSON. As for YAML, it’s purely for freshness.

Why do we use JSON to get float,int,bool, etc? Let’s look at an example:

prove

Varchar = 0.618; varchar = 0.618;

a = "0.618"
Copy the code

So how do I get float?

a = "0.618"
import json
a = json.loads(a)
print(type(a), a)
Copy the code

Try bool for the same reason:

Json is a string, so if we encounter a string type, we don’t do any json.loads, so it keeps the original flavor.

The design table

  • Env: the environment of the variable. If env is 0, the variable belongs to all environments.
  • Enable: Indicates whether a variable is available. It is a switch field that can be temporarily disabled

Note that there is a joint unique index, which is generated from env,key, and deleted_at. The database will not insert data only if env,key, and deleted_AT are all equal. This resolves a problem in the environment table:

After environment ABC is deleted, no environment named ABC can be created

The registration model

Write a schema

Write CRUD functionality

from datetime import datetime

from sqlalchemy import desc

from app.models import Session, update_model
from app.models.gconfig import GConfig
from app.models.schema.gconfig import GConfigForm
from app.utils.logger import Log


class GConfigDao(object) :
    log = Log("GConfigDao")

    @staticmethod
    def insert_gconfig(data: GConfigForm, user) :
        try:
            with Session() as session:
                query = session.query(GConfig).filter_by(env=data.env, key=data.key, deleted_at=None).first()
                if query is not None:
                    return F "variables:{data.key}The existing"
                config = GConfig(**data.dict(), user=user)
                session.add(config)
                session.commit()
        except Exception as e:
            GConfigDao.log.error(F "new variable:{data.key}Failure,{e}")
            return F "new variable:{data.key}Failure,{str(e)}"
        return None

    @staticmethod
    def update_gconfig(data: GConfigForm, user) :
        try:
            with Session() as session:
                query = session.query(GConfig).filter_by(id=data.id, deleted_at=None).first()
                if query is None:
                    return F "variable{data.key}There is no"
                update_model(query, data, user)
                session.commit()
        except Exception as e:
            GConfigDao.log.error(F "failed to edit variable:{str(e)}")
            return F "failed to edit variable:{str(e)}"
        return None

    @staticmethod
    def list_gconfig(page, size, env=None, key=None) :
        try:
            search = [GConfig.deleted_at == None]
            with Session() as session:
                if env:
                    search.append(GConfig.env == env)
                if key:
                    search.append(GConfig.name.ilike("% {} %".format(key)))
                data = session.query(GConfig).filter(*search)
                total = data.count()
                return data.order_by(desc(GConfig.created_at)).offset((page - 1) * size).limit(
                    size).all(), total, None
        except Exception as e:
            GConfigDao.log.error(F "failed to get variable list,{str(e)}")
            return[].0.F "failed to get variable list,{str(e)}"

    @staticmethod
    def delete_gconfig(id, user) :
        try:
            with Session() as session:
                query = session.query(GConfig).filter_by(id=id).first()
                if query is None:
                    return F "variable{id}There is no"
                query.deleted_at = datetime.now()
                query.update_user = user
                session.commit()
        except Exception as e:
            GConfigDao.log.error(F "Failed to delete variable:{str(e)}")
            return F "Failed to delete variable:{str(e)}"
        return None

Copy the code

Those of you who read the last article know the details. In fact, WHEN I write code, I also copy it.

Have you noticed that writing a CRUD feature is really easy? Basically can follow a fixed routine, like I am copy of the last function point of the code, and then slightly changed.

Write the core interface

from fastapi import Depends

from app.dao.config.GConfigDao import GConfigDao
from app.handler.fatcory import ResponseFactory
from app.models.schema.gconfig import GConfigForm
from app.routers import Permission
from app.routers.config.environment import router
from config import Config


@router.get("/gconfig/list")
async def list_gconfig(page: int = 1, size: int = 8, env: int = None, key: str = "", user_info=Depends(Permission())) :
    data, total, err = GConfigDao.list_gconfig(page, size, env, key)
    if err:
        return dict(code=110, msg=err)
    return dict(code=0, data=ResponseFactory.model_to_list(data), total=total, msg="Operation successful")


@router.post("/gconfig/insert")
async def insert_gconfig(data: GConfigForm, user_info=Depends(Permission(Config.ADMIN))) :
    err = GConfigDao.insert_gconfig(data, user_info['id'])
    if err:
        return dict(code=110, msg=err)
    return dict(code=0, msg="Operation successful")


@router.post("/gconfig/update")
async def update_gconfig(data: GConfigForm, user_info=Depends(Permission(Config.ADMIN))) :
    err = GConfigDao.update_gconfig(data, user_info['id'])
    if err:
        return dict(code=110, msg=err)
    return dict(code=0, msg="Operation successful")


@router.get("/gconfig/delete")
async def delete_gconfig(id: int, user_info=Depends(Permission(Config.ADMIN))) :
    err = GConfigDao.delete_gconfig(id, user_info['id'])
    if err:
        return dict(code=110, msg=err)
    return dict(code=0, msg="Operation successful")

Copy the code

The router is basically the same as the previous router, but there is one more detail:

  • What happens when there is a file under the Config router?

We’ve got a Gconfig and an Envrionment here with about 8 interfaces.

Envrionment defines ApiRouter’s Router object, and gconfig imports the router object. So will our interface be successfully registered?

The answer is no, you can try it.

Because this is the order:

  1. Router = ApiRouter() # environment
  2. pity.include_router(config.router)
  3. Router registers the interface of gconfig

Since we are running the main.py file, main.py must be the first thing that happens when we import router. Flask has this problem too, and there are two solutions.

  • Rapid method

    When registering the router in main.py, select the last one.

    For example, envrionment creates the router, and gconfig imports the router. But it’s not too safe, because you can still miss something or get it wrong.

  • Using the set p y

    Finally, import the router from init.py during registration. Simply put, it is to make a special collection of router files, and finally centralized registration.

conclusion

In addition to writing the global variable function, this issue also solved two problems:

  • Joint index problem
  • Router Registration Problem

Hope to help you ~

The online demo is at http://47.112.32.195/

Front-end repository: github.com/wuranxu/pit…

Backend repository: github.com/wuranxu/pit…

Pay attention to my personal public number test development pit goods, urge more don’t get lost.