algo-backend-framework 0.0.4__py3-none-any.whl → 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- algo_backend/exception/error_code_manage.py +10 -10
- algo_backend/handler/__init__.py +2 -1
- algo_backend/handler/simple_handler.py +11 -4
- algo_backend/handler/sse_handler.py +84 -0
- algo_backend/metrics/time_cost_metrics.py +50 -16
- algo_backend/schema/__init__.py +2 -1
- algo_backend/schema/sse.py +40 -0
- algo_backend/starter/default_app_generator.py +2 -2
- {algo_backend_framework-0.0.4.dist-info → algo_backend_framework-0.0.5.dist-info}/METADATA +1 -1
- {algo_backend_framework-0.0.4.dist-info → algo_backend_framework-0.0.5.dist-info}/RECORD +12 -10
- {algo_backend_framework-0.0.4.dist-info → algo_backend_framework-0.0.5.dist-info}/WHEEL +0 -0
- {algo_backend_framework-0.0.4.dist-info → algo_backend_framework-0.0.5.dist-info}/top_level.txt +0 -0
|
@@ -8,21 +8,21 @@ from . import status_code
|
|
|
8
8
|
from .status_code import BasicApiId, BasicApiInnerErrorCode, BasicStatusCode
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def set_error_code_prefix_env(prefix: Optional[int] = None):
|
|
12
|
+
"""
|
|
13
|
+
服务启动时执行这个函数
|
|
14
|
+
也可以启动服务时通过环境变量ERROR_CODE_SERVICE_PREFIX指定服务前缀
|
|
15
|
+
如果输入的prefix code不符合规则,则忽略,保持原来的6位
|
|
16
|
+
"""
|
|
17
|
+
if prefix and 1 <= prefix <= 99:
|
|
18
|
+
ErrorCodeConfig.SERVICE_PREFIX = prefix
|
|
19
|
+
|
|
20
|
+
|
|
11
21
|
class ApiErrorCodeManage:
|
|
12
22
|
"""
|
|
13
23
|
构建”5aaabb“错误码,aaa接口id,bb表示接口内部错误码
|
|
14
24
|
"""
|
|
15
25
|
|
|
16
|
-
@classmethod
|
|
17
|
-
def set_error_code_prefix_env(cls, prefix: Optional[int] = None):
|
|
18
|
-
"""
|
|
19
|
-
服务启动时执行这个函数
|
|
20
|
-
也可以启动服务时通过环境变量ERROR_CODE_SERVICE_PREFIX指定服务前缀
|
|
21
|
-
如果输入的prefix code不符合规则,则忽略,保持原来的6位
|
|
22
|
-
"""
|
|
23
|
-
if prefix and 1 <= prefix <= 99:
|
|
24
|
-
ErrorCodeConfig.SERVICE_PREFIX = prefix
|
|
25
|
-
|
|
26
26
|
@classmethod
|
|
27
27
|
def scan_module_and_summary(cls, *list_module) -> list:
|
|
28
28
|
"""扫描模块中的ApiId和错误码枚举类"""
|
algo_backend/handler/__init__.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import logging
|
|
2
3
|
import time
|
|
3
4
|
import traceback
|
|
@@ -9,7 +10,8 @@ from algo_backend.exception import (
|
|
|
9
10
|
DefaultApiErrorCode,
|
|
10
11
|
)
|
|
11
12
|
from algo_backend.metrics import PrometheusTimeCostMetricSetting
|
|
12
|
-
from algo_backend.schema import AbstractRespVo
|
|
13
|
+
from algo_backend.schema import AbstractRespVo, BaseRespVo
|
|
14
|
+
|
|
13
15
|
from .exception_to_vo import gen_vo_from_exception
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
@@ -20,10 +22,14 @@ def timing_and_exception_handler(
|
|
|
20
22
|
*,
|
|
21
23
|
api_id: BasicApiId = DefaultApiErrorCode.DEFAULT_ERROR,
|
|
22
24
|
api_name: str = "",
|
|
25
|
+
vo_class_type: type(AbstractRespVo) = BaseRespVo,
|
|
23
26
|
):
|
|
24
27
|
"""
|
|
25
28
|
装饰器:用于统计函数执行时间并捕获异常
|
|
26
29
|
函数中需要包含参数reqid或者request_id
|
|
30
|
+
: param api_id: 错误码
|
|
31
|
+
: param api_name: api名称
|
|
32
|
+
: param vo_class_type: 返回值类型,优先从被装饰的函数typehint中获取,如果获取类型非AbstractRespVo的实现类,则从装饰器vo_class_type中获取,默认BaseRespVo
|
|
27
33
|
"""
|
|
28
34
|
|
|
29
35
|
def decorator(
|
|
@@ -59,9 +65,10 @@ def timing_and_exception_handler(
|
|
|
59
65
|
f"Traceback:\n{traceback.format_exc()}"
|
|
60
66
|
)
|
|
61
67
|
|
|
62
|
-
vo_cls
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
vo_cls = get_type_hints(func).get("return")
|
|
69
|
+
if inspect.isclass(vo_cls) and issubclass(vo_cls, AbstractRespVo):
|
|
70
|
+
vo_cls = vo_class_type
|
|
71
|
+
|
|
65
72
|
return gen_vo_from_exception(
|
|
66
73
|
vo_cls, e, api_name=_name, request_id=request_id, api_id=api_id
|
|
67
74
|
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import traceback
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import AsyncIterable, Callable, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from algo_backend.exception import (
|
|
10
|
+
BasicApiId,
|
|
11
|
+
BasicException,
|
|
12
|
+
DefaultApiErrorCode,
|
|
13
|
+
transfer_exception,
|
|
14
|
+
)
|
|
15
|
+
from algo_backend.metrics import PrometheusTimeCostMetricSetting
|
|
16
|
+
from algo_backend.schema import SseVoGenerator
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
D = TypeVar("D", bound=BaseModel)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sse_timing_and_exception_handler(
|
|
25
|
+
transfer_obj_cls=type(SseVoGenerator),
|
|
26
|
+
*,
|
|
27
|
+
api_id: BasicApiId = DefaultApiErrorCode.DEFAULT_ERROR,
|
|
28
|
+
api_name: str = ""
|
|
29
|
+
):
|
|
30
|
+
def decorator(
|
|
31
|
+
func: Callable[..., AsyncIterable[D]],
|
|
32
|
+
) -> Callable[..., AsyncIterable[D]]:
|
|
33
|
+
@wraps(func)
|
|
34
|
+
async def wrapper(*args, **kwargs):
|
|
35
|
+
request_id = kwargs.get("request_id", None) or kwargs.get("reqid", None)
|
|
36
|
+
_name = func.__name__ or api_name
|
|
37
|
+
|
|
38
|
+
transfer_obj: SseVoGenerator = transfer_obj_cls(request_id=request_id)
|
|
39
|
+
# 提供额外参数,方便丰富上下文
|
|
40
|
+
transfer_obj.extract_info(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
start_time = time.perf_counter()
|
|
43
|
+
# 执行原函数
|
|
44
|
+
logger.info(f"ReqId: {request_id} | Function: {_name} | Start")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
start_event = transfer_obj.start()
|
|
48
|
+
if start_event:
|
|
49
|
+
yield start_event
|
|
50
|
+
|
|
51
|
+
async for item in func(*args, **kwargs):
|
|
52
|
+
yield transfer_obj.generate(item)
|
|
53
|
+
|
|
54
|
+
elapsed_time = time.perf_counter() - start_time
|
|
55
|
+
|
|
56
|
+
logger.info(
|
|
57
|
+
f"ReqId: {request_id} | Function: {_name} | COST:{elapsed_time:.4f}s"
|
|
58
|
+
)
|
|
59
|
+
PrometheusTimeCostMetricSetting.api_metrics_instance().add(
|
|
60
|
+
_name, elapsed_time
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
end_event = transfer_obj.done()
|
|
64
|
+
if end_event:
|
|
65
|
+
yield end_event
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
# 计算耗时
|
|
69
|
+
elapsed_time = time.perf_counter() - start_time
|
|
70
|
+
PrometheusTimeCostMetricSetting.api_metrics_instance().add_error(_name)
|
|
71
|
+
# 记录异常信息和完整堆栈
|
|
72
|
+
logger.error(
|
|
73
|
+
f"ReqId: {request_id} | Function: {_name} | COST:{elapsed_time:.4f}s | Exception: {str(e)}\n"
|
|
74
|
+
f"Traceback:\n{traceback.format_exc()}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
basic_exception: BasicException = transfer_exception(
|
|
78
|
+
e, api_name=_name, api_id=api_id, request_id=request_id
|
|
79
|
+
)
|
|
80
|
+
yield transfer_obj.error(basic_exception)
|
|
81
|
+
|
|
82
|
+
return wrapper
|
|
83
|
+
|
|
84
|
+
return decorator
|
|
@@ -2,7 +2,8 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
4
|
from functools import wraps
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
import inspect
|
|
6
7
|
|
|
7
8
|
from prometheus_client import Counter, Histogram
|
|
8
9
|
|
|
@@ -120,27 +121,60 @@ class PrometheusTimeCostMetricSetting:
|
|
|
120
121
|
def client_metrics_instance(cls) -> BasicTimeCostMetrics:
|
|
121
122
|
return cls.get_metrics("client")
|
|
122
123
|
|
|
124
|
+
@classmethod
|
|
125
|
+
def _create_async_gen_wrapper(cls, func, metrics_cls_name: str, key: str):
|
|
126
|
+
"""
|
|
127
|
+
创建异步生成器包装器
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
@wraps(func)
|
|
131
|
+
async def async_gen_wrapper(*args, **kwargs):
|
|
132
|
+
metrics = cls.get_metrics(metrics_cls_name)
|
|
133
|
+
start = time.perf_counter()
|
|
134
|
+
try:
|
|
135
|
+
async for item in func(*args, **kwargs):
|
|
136
|
+
yield item
|
|
137
|
+
cost = time.perf_counter() - start
|
|
138
|
+
metrics.add(key, cost)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
metrics.add_error(key)
|
|
141
|
+
raise e
|
|
142
|
+
|
|
143
|
+
return async_gen_wrapper
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def _create_async_func_wrapper(cls, func, metrics_cls_name: str, key: str):
|
|
147
|
+
"""
|
|
148
|
+
创建普通异步函数包装器
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
@wraps(func)
|
|
152
|
+
async def async_func_wrapper(*args, **kwargs):
|
|
153
|
+
metrics = cls.get_metrics(metrics_cls_name)
|
|
154
|
+
start = time.perf_counter()
|
|
155
|
+
try:
|
|
156
|
+
result = await func(*args, **kwargs)
|
|
157
|
+
metrics.add(key, time.perf_counter() - start)
|
|
158
|
+
return result
|
|
159
|
+
except Exception as e:
|
|
160
|
+
metrics.add_error(key)
|
|
161
|
+
raise e
|
|
162
|
+
|
|
163
|
+
return async_func_wrapper
|
|
164
|
+
|
|
123
165
|
@classmethod
|
|
124
166
|
def metrics_handler(cls, key: str, metrics_cls_name: str):
|
|
125
167
|
"""
|
|
126
168
|
装饰器只会初始化一次,因此要使用动态方式获取指标类,否则初始化装饰器时,指标类还没有被初始化
|
|
127
169
|
"""
|
|
128
170
|
|
|
129
|
-
def decorator(func
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
cost = time.perf_counter() - start
|
|
137
|
-
metrics.add(key, cost)
|
|
138
|
-
return result
|
|
139
|
-
except Exception as e:
|
|
140
|
-
metrics.add_error(key)
|
|
141
|
-
raise e
|
|
142
|
-
|
|
143
|
-
return wrapper
|
|
171
|
+
def decorator(func):
|
|
172
|
+
if inspect.isasyncgenfunction(func):
|
|
173
|
+
return cls._create_async_gen_wrapper(func, metrics_cls_name, key)
|
|
174
|
+
elif inspect.iscoroutinefunction(func):
|
|
175
|
+
return cls._create_async_func_wrapper(func, metrics_cls_name, key)
|
|
176
|
+
else:
|
|
177
|
+
raise ValueError(f"{func.__name__} is not a async function or async generator")
|
|
144
178
|
|
|
145
179
|
return decorator
|
|
146
180
|
|
algo_backend/schema/__init__.py
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import TypeVar, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from algo_backend.exception import BasicException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=BaseModel)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SseVoGenerator(ABC):
|
|
13
|
+
def __init__(self, request_id: str=None):
|
|
14
|
+
self.request_id = request_id
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def extract_info(self, *args, **kwargs):
|
|
18
|
+
"""额外获取信息"""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def start(self) -> Optional[T]:
|
|
23
|
+
"""
|
|
24
|
+
返回None时,sse_timing_and_exception_handler会忽略这一步
|
|
25
|
+
"""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def generate(self, content: BaseModel) -> T: ...
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def done(self) -> T:
|
|
33
|
+
"""
|
|
34
|
+
返回None时,sse_timing_and_exception_handler会忽略这一步
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def error(self, exception: BasicException) -> T:
|
|
40
|
+
...
|
|
@@ -6,7 +6,7 @@ from fastapi import FastAPI
|
|
|
6
6
|
from fastapi.exceptions import RequestValidationError, StarletteHTTPException
|
|
7
7
|
from starlette.middleware import Middleware
|
|
8
8
|
|
|
9
|
-
from algo_backend.exception import
|
|
9
|
+
from algo_backend.exception.error_code_manage import set_error_code_prefix_env
|
|
10
10
|
from algo_backend.intercept import (
|
|
11
11
|
BasicExceptionInterceptor,
|
|
12
12
|
HTTPExceptionInterceptor,
|
|
@@ -55,7 +55,7 @@ class DefaultAlgoAppGenerator:
|
|
|
55
55
|
:param client_time_cost_buckets: 客户端耗时统计的桶,None表示使用默认的,空列表表示不启用这个指标
|
|
56
56
|
"""
|
|
57
57
|
# 注册8位错误码前缀
|
|
58
|
-
|
|
58
|
+
set_error_code_prefix_env(app_error_code_prefix)
|
|
59
59
|
self.service_name = service_name
|
|
60
60
|
|
|
61
61
|
self.__app: Optional[FastAPI] = FastAPI()
|
|
@@ -3,12 +3,13 @@ algo_backend/config/__init__.py,sha256=MLAvcvodxXiaVsSJaVvQcNjFWIFusBQ0i9OrWJv3z
|
|
|
3
3
|
algo_backend/config/basic_config.py,sha256=RSRf8lgc8KfA_FHNjchaghSLNnJy7ejn2ux57SBSakg,292
|
|
4
4
|
algo_backend/config/loguru_config.py,sha256=WovghKcMqeysPuf8vlKFhbNug8Z7zPqxkjlwVYKjZWw,513
|
|
5
5
|
algo_backend/exception/__init__.py,sha256=tHC3p8DyCrFHW4WvLEOuPKP5t4yVqhc1flcWQRtz5AY,653
|
|
6
|
-
algo_backend/exception/error_code_manage.py,sha256=
|
|
6
|
+
algo_backend/exception/error_code_manage.py,sha256=7aBGvwepAmZe1VgW87-2_JbBD6eJrmZD6tmsgdczk1k,4338
|
|
7
7
|
algo_backend/exception/exception.py,sha256=Gz5LDsqRct8LON1Aq43TdqQQjyC4_Uy9yeUlO7XtPnM,1962
|
|
8
8
|
algo_backend/exception/status_code.py,sha256=s5lXXR-oSALk3gtI4KcPKHstvnRFArQL1SEgQFIeSxU,2707
|
|
9
|
-
algo_backend/handler/__init__.py,sha256=
|
|
9
|
+
algo_backend/handler/__init__.py,sha256=DBUeFrDVm8qhFPahiva93HeYuViPIFL2ZCHg26iupFU,199
|
|
10
10
|
algo_backend/handler/exception_to_vo.py,sha256=uX3JZaPP5Zt1RgJunFfumxpSfWHwcWtKLZ21dIXDraQ,687
|
|
11
|
-
algo_backend/handler/simple_handler.py,sha256=
|
|
11
|
+
algo_backend/handler/simple_handler.py,sha256=1Pqpgt-ZKY2ytSAlAXOEqI55C5yein-XSePPD6F8zH4,3010
|
|
12
|
+
algo_backend/handler/sse_handler.py,sha256=NR0RiqYYmNAo57J3qvlbKkIGme3zSq7x2L8enT6sIVs,2844
|
|
12
13
|
algo_backend/intercept/__init__.py,sha256=FoNHCzUc3ceLo85ECN3L7HzW6KmLqcG5sgMh_qULLdw,265
|
|
13
14
|
algo_backend/intercept/common.py,sha256=T50_IAeY0HQ8TbupjYvMHMObg3j9I4lQnqZMiWNzuQw,1445
|
|
14
15
|
algo_backend/intercept/http.py,sha256=C_N2nyErFhdOZ1LPQ6iU_JCy4fFYEucDwhWJb-qBnbc,1381
|
|
@@ -24,7 +25,7 @@ algo_backend/log/nblog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
24
25
|
algo_backend/metrics/__init__.py,sha256=20CPEFgi0yJ2Fy-LTcFQ3aqlXHhLPtgPFboqOMBhl34,640
|
|
25
26
|
algo_backend/metrics/http_metrics.py,sha256=3GYNhKJoS6oJ8L7FXnVkPZ_wa-SKj_444KLdBeGkbs4,1944
|
|
26
27
|
algo_backend/metrics/prometheus_context.py,sha256=GqemLccNbTx_5DBijOyzySzWydbpWYfnhWh17rujjwE,1910
|
|
27
|
-
algo_backend/metrics/time_cost_metrics.py,sha256=
|
|
28
|
+
algo_backend/metrics/time_cost_metrics.py,sha256=8a2GgFzeAmf0x4c203C2R5xliza4UIsWE_BwZ8ug3tI,6011
|
|
28
29
|
algo_backend/metrics/collector/__init__.py,sha256=t5Csl-Z9qpik-isEUVYDYBWYgf7xdM49548YfJr4X64,413
|
|
29
30
|
algo_backend/metrics/collector/common.py,sha256=yeiZp9ufbYSW7N-R6ytk74UF0dCqJ8r338lL64TmA4g,475
|
|
30
31
|
algo_backend/metrics/collector/gc_metrics.py,sha256=s2ariKimji9LC71eAQFlrFkpcF3hkSy5IBC6D8LFePc,2510
|
|
@@ -33,16 +34,17 @@ algo_backend/metrics/collector/system_metrics.py,sha256=SxOswHKGKLDB5wMsKjd7rkQt
|
|
|
33
34
|
algo_backend/middleware/__init__.py,sha256=KfSRT3TZ89BzY__Rrbu-9HkjO1LToD5sXyl_J4Po7Q4,173
|
|
34
35
|
algo_backend/middleware/cors.py,sha256=kKdPeZMS38IVvOueJz34lQX1t1hH5OOz068IdIAR-Kc,271
|
|
35
36
|
algo_backend/middleware/metrics.py,sha256=XwZGipQwKp20naZglx41Wc79rXud81Y4AIfswUJj3jM,385
|
|
36
|
-
algo_backend/schema/__init__.py,sha256=
|
|
37
|
+
algo_backend/schema/__init__.py,sha256=WhbHgUnPG3B69qJlo-lAmtRhu_LfSwjkBbByLASNcFY,141
|
|
38
|
+
algo_backend/schema/sse.py,sha256=OfVldmgIISvf9xeTurmDp8HDwAN12WrQtZ81JYYA0JI,938
|
|
37
39
|
algo_backend/schema/vo.py,sha256=CSYNHpJx2YNCZ5-4GjjSs6BNKiJEyl31gnplnxGvXfY,2211
|
|
38
40
|
algo_backend/starter/__init__.py,sha256=2AfidtR7PmlhVPz65tt_t3QdIYqpp0mVg8M-lbJE2F4,194
|
|
39
|
-
algo_backend/starter/default_app_generator.py,sha256=
|
|
41
|
+
algo_backend/starter/default_app_generator.py,sha256=KEKVzzb7duZlEkwYsCqum4OmL9p6BfgXK28kt1C8F7k,6215
|
|
40
42
|
algo_backend/starter/default_service_starter.py,sha256=QHVDAS4css7tYCH7Fd_is2uWX8fxV1GD7LAugx904B8,2017
|
|
41
43
|
algo_backend/starter/event_list.py,sha256=vQHzQIpW8LZmQ93YyET-1gX6pQGVE5A6I_pLoYTFOH0,824
|
|
42
44
|
algo_backend/utils/__init__.py,sha256=oX6OyL-28jzc94u4fyH1TtntCzQySkfZ8jibMk1KPU8,168
|
|
43
45
|
algo_backend/utils/meta_class.py,sha256=hcZPGF7EIHvJOXXR82_7Gah_AWbqkcSxUc473I_6maY,1850
|
|
44
46
|
algo_backend/utils/utils.py,sha256=q3bxBrivndLRggWsLryloSpu-_Ecbj4mhZL8oYdiDTo,481
|
|
45
|
-
algo_backend_framework-0.0.
|
|
46
|
-
algo_backend_framework-0.0.
|
|
47
|
-
algo_backend_framework-0.0.
|
|
48
|
-
algo_backend_framework-0.0.
|
|
47
|
+
algo_backend_framework-0.0.5.dist-info/METADATA,sha256=VVoWXuVfk-ZnviGbsLKhdM5Iyk2hh5V9wP39vLp-mU0,2196
|
|
48
|
+
algo_backend_framework-0.0.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
49
|
+
algo_backend_framework-0.0.5.dist-info/top_level.txt,sha256=zLsbLTRV1tO2hQfazqiBLO73VnjSAhJSUpMMBmQaLfw,13
|
|
50
|
+
algo_backend_framework-0.0.5.dist-info/RECORD,,
|
|
File without changes
|
{algo_backend_framework-0.0.4.dist-info → algo_backend_framework-0.0.5.dist-info}/top_level.txt
RENAMED
|
File without changes
|