commit cb1a26b2705a5ceef54fb184053a1f1b684b2cce Author: Cxx0822 <1556464090@qq.com> Date: Sun Oct 19 22:16:27 2025 +0800 feat:初始化工程 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/log-alert-service.iml b/.idea/log-alert-service.iml new file mode 100644 index 0000000..74d515a --- /dev/null +++ b/.idea/log-alert-service.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f447362 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d88adc6 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..1c2a5f3 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/apis/__init__.py b/apis/__init__.py new file mode 100644 index 0000000..e751abf --- /dev/null +++ b/apis/__init__.py @@ -0,0 +1,6 @@ +from fastapi import FastAPI +from .log import router as log_router + + +def register_routers(app: FastAPI): + app.include_router(log_router, prefix="") diff --git a/apis/__pycache__/__init__.cpython-312.pyc b/apis/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1247962 Binary files /dev/null and b/apis/__pycache__/__init__.cpython-312.pyc differ diff --git a/apis/__pycache__/log.cpython-312.pyc b/apis/__pycache__/log.cpython-312.pyc new file mode 100644 index 0000000..703c24d Binary files /dev/null and b/apis/__pycache__/log.cpython-312.pyc differ diff --git a/apis/log.py b/apis/log.py new file mode 100644 index 0000000..1bfcc1d --- /dev/null +++ b/apis/log.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter + +from schemas.log_alert import LogAlertTrigger +from service import log_alert_service + +router = APIRouter( + prefix="/log-alter", + tags=["日志告警"], + responses={404: {"description": "Not found"}} +) + + +@router.post("", summary="触发日志告警") +def trigger_log_alter(log_alter: LogAlertTrigger): + return log_alert_service.trigger_alert(log_alter) diff --git a/config/__pycache__/database.cpython-312.pyc b/config/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..bedea5f Binary files /dev/null and b/config/__pycache__/database.cpython-312.pyc differ diff --git a/config/__pycache__/setting.cpython-312.pyc b/config/__pycache__/setting.cpython-312.pyc new file mode 100644 index 0000000..ca8ddbf Binary files /dev/null and b/config/__pycache__/setting.cpython-312.pyc differ diff --git a/config/database.py b/config/database.py new file mode 100644 index 0000000..3440129 --- /dev/null +++ b/config/database.py @@ -0,0 +1,26 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from config.setting import settings +from models.log_alert import Base + +engine = create_engine( + settings.database_url, connect_args={"check_same_thread": False} +) + +# 会话工厂 +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def init_db(): + Base.metadata.create_all(bind=engine) + + +def get_db(): + # 创建数据库会话实例 + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/config/setting.py b/config/setting.py new file mode 100644 index 0000000..1ee8784 --- /dev/null +++ b/config/setting.py @@ -0,0 +1,18 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # sqlite连接方式:sqlite:/// + database_url: str = "sqlite:///./logs.db" + wechat_webhook: str = "" + smtp_server: str = "" + smtp_port: int = 587 + smtp_user: str = "" + smtp_password: str = "" + smtp_sender: str = "log-alert@example.com" + + class Config: + env_file = ".env" + + +settings = Settings() \ No newline at end of file diff --git a/id_generator/__init__.py b/id_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/id_generator/__pycache__/__init__.cpython-312.pyc b/id_generator/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..934c604 Binary files /dev/null and b/id_generator/__pycache__/__init__.cpython-312.pyc differ diff --git a/id_generator/__pycache__/generator.cpython-312.pyc b/id_generator/__pycache__/generator.cpython-312.pyc new file mode 100644 index 0000000..3ef7f83 Binary files /dev/null and b/id_generator/__pycache__/generator.cpython-312.pyc differ diff --git a/id_generator/__pycache__/options.cpython-312.pyc b/id_generator/__pycache__/options.cpython-312.pyc new file mode 100644 index 0000000..781eaf9 Binary files /dev/null and b/id_generator/__pycache__/options.cpython-312.pyc differ diff --git a/id_generator/__pycache__/snowflake.cpython-312.pyc b/id_generator/__pycache__/snowflake.cpython-312.pyc new file mode 100644 index 0000000..6fca833 Binary files /dev/null and b/id_generator/__pycache__/snowflake.cpython-312.pyc differ diff --git a/id_generator/__pycache__/snowflake_m1.cpython-312.pyc b/id_generator/__pycache__/snowflake_m1.cpython-312.pyc new file mode 100644 index 0000000..9e662f0 Binary files /dev/null and b/id_generator/__pycache__/snowflake_m1.cpython-312.pyc differ diff --git a/id_generator/generator.py b/id_generator/generator.py new file mode 100644 index 0000000..fc995d7 --- /dev/null +++ b/id_generator/generator.py @@ -0,0 +1,38 @@ +""" +雪花算法生成器IdGenerator +""" + +# !/usr/bin/python +# coding=UTF-8 + + +from . import options +from . import snowflake_m1 + + +class DefaultIdGenerator: + """ + ID生成器 + """ + + def __init__(self): + self.snowflake = None + + def set_id_generator(self, option: options.IdGeneratorOptions): + """ + 设置id生成规则信息 + """ + + if option.base_time < 100000: + raise ValueError("base time error.") + + self.snowflake = snowflake_m1.SnowFlakeM1(option) + + def next_id(self) -> int: + """ + 获取新的UUID + """ + + if self.snowflake is None: + raise ValueError("please set id generator at first.") + return self.snowflake.next_id() diff --git a/id_generator/idregister.py b/id_generator/idregister.py new file mode 100644 index 0000000..787faac --- /dev/null +++ b/id_generator/idregister.py @@ -0,0 +1,134 @@ +""" +worker id generator +""" + +# !/usr/bin/python +# coding=UTF-8 + + +from threading import Thread +import time +import logging +import redis + + +class Register: + """ + redis封装 + - host 代表redis ip + - port 代表redis端口 + - max_worker_id worker_id的最大值, 默认为100 + - password redis的密码, 默认为空 + """ + + def __init__(self, host, port, max_worker_id=100, password=None): + self.redis_impl = redis.StrictRedis(host=host, port=port, db=0, password=password) + self.loop_count = 0 + self.max_loop_count = 10 + self.worker_id_expire_time = 15 + self.max_worker_id = max_worker_id + self.worker_id = -1 + self.is_stop = False + + def get_lock(self, key): + """ + 获取分布式全局锁,并设置过期时间为30秒 + """ + + if self.redis_impl.setnx(key, 1): + self.redis_impl.expire(key, 30) + return True + if self.redis_impl.ttl(key) < 0: + self.redis_impl.expire(key, 30) + return False + + def stop(self): + """ + 退出注册器的线程 + """ + + self.is_stop = True + + def get_worker_id(self): + """ + 获取全局唯一worker_id, 会创建一个线程给worker id续期 + 失败返回-1 + """ + + self.loop_count = 0 + + def extern_life(my_id): + while 1: + time.sleep(self.worker_id_expire_time / 3) + # 是否关闭了 + if self.is_stop: + return + # 更新生命周期 + if self.worker_id != my_id: + break + try: + self.redis_impl.expire( + f"IdGen:WorkerId:Value:{my_id}", + self.worker_id_expire_time) + except Exception as exe: + logging.error(exe) + continue + + self.worker_id = self.__get_next_worker_id() + if self.worker_id > -1: + Thread(target=extern_life, args=[self.worker_id]).start() + return self.worker_id + + def __get_next_worker_id(self): + """ + 获取全局唯一worker id内部实现 + """ + + cur = self.redis_impl.incrby("IdGen:WorkerId:Index", 1) + + def can_reset(): + try: + reset_value = self.redis_impl.incr("IdGen:WorkerId:Value:Edit") + return reset_value != 1 + except Exception as ept: + logging.error(ept) + return False + + def end_reset(): + try: + self.redis_impl.set("IdGen:WorkerId:Value:Edit", 0) + except Exception as ept: + logging.error(ept) + + def is_available(worker_id: int): + try: + rst = self.redis_impl.get(f"IdGen:WorkerId:Value:{worker_id}") + return rst != "Y" + except Exception as ept: + logging.error(ept) + return False + + if cur > self.max_worker_id: + if can_reset(): + self.redis_impl.set("IdGen:WorkerId:Index", -1) + end_reset() + self.loop_count += 1 + + if self.loop_count > self.max_loop_count: + self.loop_count = 0 + return -1 + + time.sleep(0.2 * self.loop_count) + return self.__get_next_worker_id() + time.sleep(0.2) + return self.__get_next_worker_id() + if is_available(cur): + self.redis_impl.setex( + f"IdGen:WorkerId:Value:{cur}", + self.worker_id_expire_time, + "Y" + ) + self.loop_count = 0 + return cur + + return self.__get_next_worker_id() diff --git a/id_generator/options.py b/id_generator/options.py new file mode 100644 index 0000000..8558381 --- /dev/null +++ b/id_generator/options.py @@ -0,0 +1,43 @@ +""" +生成器IdGenerator配置选项 +""" + +# !/usr/bin/python +# coding=UTF-8 + + +class IdGeneratorOptions: + """ + ID生成器配置 + - worker_id 全局唯一id, 区分不同uuid生成器实例 + - worker_id_bit_length 生成的uuid中worker_id占用的位数 + - seq_bit_length 生成的uuid中序列号占用的位数 + """ + + def __init__(self, worker_id=0, worker_id_bit_length=6, seq_bit_length=6): + + # 雪花计算方法,(1-漂移算法|2-传统算法), 默认1。目前只实现了1。 + self.method = 1 + + # 基础时间(ms单位), 不能超过当前系统时间 + self.base_time = 1582136402000 + + # 机器码, 必须由外部设定, 最大值 2^worker_id_bit_length-1 + self.worker_id = worker_id + + # 机器码位长, 默认值6, 取值范围 [1, 15](要求:序列数位长+机器码位长不超过22) + self.worker_id_bit_length = worker_id_bit_length + + # 序列数位长, 默认值6, 取值范围 [3, 21](要求:序列数位长+机器码位长不超过22) + self.seq_bit_length = seq_bit_length + + # 最大序列数(含), 设置范围 [max_seq_number, 2^seq_bit_length-1] + # 默认值0, 表示最大序列数取最大值(2^seq_bit_length-1]) + self.max_seq_number = 0 + + # 最小序列数(含), 默认值5, 取值范围 [5, max_seq_number], 每毫秒的前5个序列数对应编号0-4是保留位 + # 其中1-4是时间回拨相应预留位, 0是手工新值预留位 + self.min_seq_number = 5 + + # 最大漂移次数(含), 默认2000, 推荐范围500-10000(与计算能力有关) + self.top_over_cost_count = 2000 diff --git a/id_generator/snowflake.py b/id_generator/snowflake.py new file mode 100644 index 0000000..ea36cfa --- /dev/null +++ b/id_generator/snowflake.py @@ -0,0 +1,20 @@ + +""" +雪花算法生成器接口声明 +""" + +# !/usr/bin/python +# coding=UTF-8 + + +class SnowFlake(): + + def __init__(self, options): + self.options = options + + def next_id(self) -> int: + """ + 获取新的UUID + """ + + return 0 diff --git a/id_generator/snowflake_m1.py b/id_generator/snowflake_m1.py new file mode 100644 index 0000000..596b76d --- /dev/null +++ b/id_generator/snowflake_m1.py @@ -0,0 +1,147 @@ +""" +M1生成器 +""" + +# !/usr/bin/python +# coding=UTF-8 + +import threading +import time +from .snowflake import SnowFlake +from .options import IdGeneratorOptions + + +class SnowFlakeM1(SnowFlake): + """ + M1规则ID生成器配置 + """ + + def __init__(self, options: IdGeneratorOptions): + # 1.base_time + self.base_time = 1582136402000 + if options.base_time != 0: + self.base_time = int(options.base_time) + + # 2.worker_id_bit_length + self.worker_id_bit_length = 6 + if options.worker_id_bit_length != 0: + self.worker_id_bit_length = int(options.worker_id_bit_length) + + # 3.worker_id + self.worker_id = options.worker_id + + # 4.seq_bit_length + self.seq_bit_length = 6 + if options.seq_bit_length != 0: + self.seq_bit_length = int(options.seq_bit_length) + + # 5.max_seq_number + self.max_seq_number = int(options.max_seq_number) + if options.max_seq_number <= 0: + self.max_seq_number = (1 << self.seq_bit_length) - 1 + + # 6.min_seq_number + self.min_seq_number = int(options.min_seq_number) + + # 7.top_over_cost_count + self.top_over_cost_count = int(options.top_over_cost_count) + + # 8.Others + self.__timestamp_shift = self.worker_id_bit_length + self.seq_bit_length + self.__current_seq_number = self.min_seq_number + self.__last_time_tick: int = 0 + self.__turn_back_time_tick: int = 0 + self.__turn_back_index: int = 0 + self.__is_over_cost = False + self.___over_cost_count_in_one_term: int = 0 + self.__id_lock = threading.Lock() + + def __next_over_cost_id(self) -> int: + current_time_tick = self.__get_current_time_tick() + if current_time_tick > self.__last_time_tick: + self.__last_time_tick = current_time_tick + self.__current_seq_number = self.min_seq_number + self.__is_over_cost = False + self.___over_cost_count_in_one_term = 0 + return self.__calc_id(self.__last_time_tick) + + if self.___over_cost_count_in_one_term >= self.top_over_cost_count: + self.__last_time_tick = self.__get_next_time_tick() + self.__current_seq_number = self.min_seq_number + self.__is_over_cost = False + self.___over_cost_count_in_one_term = 0 + return self.__calc_id(self.__last_time_tick) + + if self.__current_seq_number > self.max_seq_number: + self.__last_time_tick += 1 + self.__current_seq_number = self.min_seq_number + self.__is_over_cost = True + self.___over_cost_count_in_one_term += 1 + return self.__calc_id(self.__last_time_tick) + + return self.__calc_id(self.__last_time_tick) + + def __next_normal_id(self) -> int: + current_time_tick = self.__get_current_time_tick() + if current_time_tick < self.__last_time_tick: + if self.__turn_back_time_tick < 1: + self.__turn_back_time_tick = self.__last_time_tick - 1 + self.__turn_back_index += 1 + # 每毫秒序列数的前5位是预留位, 0用于手工新值, 1-4是时间回拨次序 + # 支持4次回拨次序(避免回拨重叠导致ID重复), 可无限次回拨(次序循环使用)。 + if self.__turn_back_index > 4: + self.__turn_back_index = 1 + + return self.__calc_turn_back_id(self.__turn_back_time_tick) + + # 时间追平时, _TurnBackTimeTick清零 + self.__turn_back_time_tick = min(self.__turn_back_time_tick, 0) + + if current_time_tick > self.__last_time_tick: + self.__last_time_tick = current_time_tick + self.__current_seq_number = self.min_seq_number + return self.__calc_id(self.__last_time_tick) + + if self.__current_seq_number > self.max_seq_number: + self.__last_time_tick += 1 + self.__current_seq_number = self.min_seq_number + self.__is_over_cost = True + self.___over_cost_count_in_one_term = 1 + return self.__calc_id(self.__last_time_tick) + + return self.__calc_id(self.__last_time_tick) + + def __calc_id(self, use_time_tick) -> int: + self.__current_seq_number += 1 + return ( + (use_time_tick << self.__timestamp_shift) + + (self.worker_id << self.seq_bit_length) + + self.__current_seq_number + ) % int(1e64) + + def __calc_turn_back_id(self, use_time_tick) -> int: + self.__turn_back_time_tick -= 1 + return ( + (use_time_tick << self.__timestamp_shift) + + (self.worker_id << self.seq_bit_length) + + self.__turn_back_index + ) % int(1e64) + + def __get_current_time_tick(self) -> int: + return int((time.time_ns() / 1e6) - self.base_time) + + def __get_next_time_tick(self) -> int: + temp_time_ticker = self.__get_current_time_tick() + while temp_time_ticker <= self.__last_time_tick: + # 0.001 = 1 mili sec + time.sleep(0.001) + temp_time_ticker = self.__get_current_time_tick() + return temp_time_ticker + + def next_id(self) -> int: + with self.__id_lock: + if self.__is_over_cost: + nextid = self.__next_over_cost_id() + else: + nextid = self.__next_normal_id() + return nextid diff --git a/logs.db b/logs.db new file mode 100644 index 0000000..763fad3 Binary files /dev/null and b/logs.db differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..0bbad46 --- /dev/null +++ b/main.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI + +from apis import register_routers +from config.database import init_db + +app = FastAPI(title="Log Alert Service") + + +@app.on_event("startup") +def on_startup(): + init_db() + + +register_routers(app) diff --git a/models/__pycache__/base.cpython-312.pyc b/models/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000..bb5d27c Binary files /dev/null and b/models/__pycache__/base.cpython-312.pyc differ diff --git a/models/__pycache__/log_alert.cpython-312.pyc b/models/__pycache__/log_alert.cpython-312.pyc new file mode 100644 index 0000000..b3d96b7 Binary files /dev/null and b/models/__pycache__/log_alert.cpython-312.pyc differ diff --git a/models/base.py b/models/base.py new file mode 100644 index 0000000..d35671e --- /dev/null +++ b/models/base.py @@ -0,0 +1,40 @@ +from sqlalchemy import Column, BigInteger, DateTime, event +from sqlalchemy.ext.declarative import declared_attr, declarative_base + +from datetime import datetime + +from config.database import Base +from utils.common import camel_to_snake +from id_generator import options, generator + +# https://github.com/yitter/IdGenerator/tree/master/Python +options = options.IdGeneratorOptions(worker_id=23) +idgen = generator.DefaultIdGenerator() +idgen.set_id_generator(options) + + +# 第二层基类:包含ID +class IdBase(Base): + __abstract__ = True + + id = Column(BigInteger, primary_key=True, index=True) + + @declared_attr + def __tablename__(cls): + # 自动把数据库实体类名驼峰转为数据库表名下划线 + return camel_to_snake(cls.__name__) + + +# 自动填充id +@event.listens_for(IdBase, 'before_insert', propagate=True) +def before_insert_listener(mapper, connection, target): + if target.id is None: + target.id = idgen.next_id() + + +# 第二层基类:包含ID和审计字段 +class AuditBase(IdBase): + __abstract__ = True + + create_time = Column(DateTime, nullable=True, default=datetime.now) + update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now) diff --git a/models/log_alert.py b/models/log_alert.py new file mode 100644 index 0000000..6d8f97e --- /dev/null +++ b/models/log_alert.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, Integer, String, DateTime, Text +from sqlalchemy.ext.declarative import declarative_base +from datetime import datetime + +Base = declarative_base() + + +class LogAlert(Base): + __tablename__ = "log_alerts" + + id = Column(Integer, primary_key=True, index=True) + timestamp = Column(DateTime) + log_level = Column(String(20)) + message = Column(Text) + source = Column(String(100)) + context = Column(Text) # 存储JSON格式的上下文信息 + status = Column(String(20), default="pending") # pending, notified, resolved diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e2eac61 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi~=0.119.0 +pydantic~=2.12.2 +SQLAlchemy~=2.0.44 +requests~=2.32.5 \ No newline at end of file diff --git a/schemas/__pycache__/log_alert.cpython-312.pyc b/schemas/__pycache__/log_alert.cpython-312.pyc new file mode 100644 index 0000000..77618c4 Binary files /dev/null and b/schemas/__pycache__/log_alert.cpython-312.pyc differ diff --git a/schemas/log_alert.py b/schemas/log_alert.py new file mode 100644 index 0000000..b626702 --- /dev/null +++ b/schemas/log_alert.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, Field + + +class LogAlertTrigger(BaseModel): + alter_name: str + timestamp: int + + +class OpenobserveQuery(BaseModel): + start_time: int + end_time: int + from_: int = Field(default=0, alias="from") + size: int = 10 + sql: str = "select * from default" diff --git a/service/__pycache__/log_alert_service.cpython-312.pyc b/service/__pycache__/log_alert_service.cpython-312.pyc new file mode 100644 index 0000000..bfd3c6f Binary files /dev/null and b/service/__pycache__/log_alert_service.cpython-312.pyc differ diff --git a/service/__pycache__/openobserve_service.cpython-312.pyc b/service/__pycache__/openobserve_service.cpython-312.pyc new file mode 100644 index 0000000..54bdd0c Binary files /dev/null and b/service/__pycache__/openobserve_service.cpython-312.pyc differ diff --git a/service/log_alert_service.py b/service/log_alert_service.py new file mode 100644 index 0000000..2174280 --- /dev/null +++ b/service/log_alert_service.py @@ -0,0 +1,31 @@ +import requests +from requests.auth import HTTPBasicAuth + +from schemas.log_alert import LogAlertTrigger, OpenobserveQuery +from service import openobserve_service + + +def trigger_alert(log_alter: LogAlertTrigger): + #print(log_alter) + # openobserve_service.get_log() + + url = 'http://192.168.1.7:5080/api/default/_search' + username = 'njcxx0822@163.com' + password = '19940822Cxx' + + response = requests.post( + url, + headers={ + 'accept': 'application/json', + 'Content-Type': 'application/json' + }, + json={ + "query": OpenobserveQuery( + start_time=1760773184953420, + end_time=1760773189953431 + ).model_dump() + }, + auth=HTTPBasicAuth(username, password) + ) + + return response.json().get("hits") diff --git a/service/openobserve_service.py b/service/openobserve_service.py new file mode 100644 index 0000000..961d813 --- /dev/null +++ b/service/openobserve_service.py @@ -0,0 +1,27 @@ +import requests +from requests.auth import HTTPBasicAuth + +from schemas.log_alert import OpenobserveQuery + + +def get_log(): + url = 'http://192.168.1.7:5080/api/default/_search' + username = 'njcxx0822@163.com' + password = '19940822Cxx' + + response = requests.post( + url, + headers={ + 'accept': 'application/json', + 'Content-Type': 'application/json' + }, + json={ + "query": OpenobserveQuery( + start_time=1760773184953420, + end_time=1760773189953431 + ).model_dump() + }, + auth=HTTPBasicAuth(username, password) + ) + + print(response) diff --git a/test_main.http b/test_main.http new file mode 100644 index 0000000..a2d81a9 --- /dev/null +++ b/test_main.http @@ -0,0 +1,11 @@ +# Test your FastAPI endpoints + +GET http://127.0.0.1:8000/ +Accept: application/json + +### + +GET http://127.0.0.1:8000/hello/User +Accept: application/json + +### diff --git a/utils/__pycache__/common.cpython-312.pyc b/utils/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000..2e14836 Binary files /dev/null and b/utils/__pycache__/common.cpython-312.pyc differ diff --git a/utils/common.py b/utils/common.py new file mode 100644 index 0000000..bb599fe --- /dev/null +++ b/utils/common.py @@ -0,0 +1,13 @@ +import re + + +def camel_to_snake(name: str) -> str: + """将驼峰命名转换为蛇形命名(CamelCase → snake_case)""" + name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() + + +def snake_to_camel(name: str) -> str: + """将蛇形命名转换为驼峰命名(snake_case → CamelCase)""" + components = name.split('_') + return ''.join(x.title() for x in components)