fustor-common 0.1.8__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.
- fustor_common/__init__.py +4 -0
- fustor_common/daemon.py +52 -0
- fustor_common/daemon_launcher.py +92 -0
- fustor_common/enums.py +5 -0
- fustor_common/exceptions.py +3 -0
- fustor_common/logging_config.py +161 -0
- fustor_common/models.py +75 -0
- fustor_common/paths.py +13 -0
- fustor_common/schemas.py +9 -0
- fustor_common-0.1.8.dist-info/METADATA +9 -0
- fustor_common-0.1.8.dist-info/RECORD +13 -0
- fustor_common-0.1.8.dist-info/WHEEL +5 -0
- fustor_common-0.1.8.dist-info/top_level.txt +1 -0
fustor_common/daemon.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common daemon functionality for Fustor services.
|
|
3
|
+
This module provides a generic daemon launcher that can be used by
|
|
4
|
+
any Fustor service without knowing implementation details.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import subprocess
|
|
9
|
+
from fustor_common.paths import get_fustor_home_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def start_daemon(service_module_path, app_var_name, pid_file_name, log_file_name, display_name, port, host='127.0.0.1', verbose=False, reload=False):
|
|
13
|
+
"""
|
|
14
|
+
Start a Fustor service as a daemon process.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
service_module_path (str): The Python module path to import (e.g., 'fustor_registry.main')
|
|
18
|
+
app_var_name (str): The name of the application variable in the module (e.g., 'app')
|
|
19
|
+
pid_file_name (str): The name of the PID file (e.g., 'registry.pid')
|
|
20
|
+
log_file_name (str): The name of the log file (e.g., 'registry.log')
|
|
21
|
+
display_name (str): The display name for the service (e.g., 'Fustor Registry')
|
|
22
|
+
port (int): The port to run the service on
|
|
23
|
+
verbose (bool): Whether to enable verbose logging
|
|
24
|
+
"""
|
|
25
|
+
# Use the generic daemon launcher script
|
|
26
|
+
daemon_script_path = os.path.join(os.path.dirname(__file__), 'daemon_launcher.py')
|
|
27
|
+
|
|
28
|
+
# Create the command to execute the generic daemon launcher
|
|
29
|
+
command = [
|
|
30
|
+
sys.executable,
|
|
31
|
+
daemon_script_path,
|
|
32
|
+
service_module_path,
|
|
33
|
+
app_var_name,
|
|
34
|
+
pid_file_name,
|
|
35
|
+
log_file_name,
|
|
36
|
+
display_name,
|
|
37
|
+
str(port),
|
|
38
|
+
host
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
if verbose:
|
|
42
|
+
command.append('--verbose')
|
|
43
|
+
|
|
44
|
+
if reload:
|
|
45
|
+
command.append('--reload')
|
|
46
|
+
|
|
47
|
+
# Set up the environment to ensure the subprocess has correct paths
|
|
48
|
+
env = os.environ.copy()
|
|
49
|
+
# Ensure PYTHONPATH includes the current directory for development
|
|
50
|
+
env['PYTHONPATH'] = os.getcwd() + ':' + env.get('PYTHONPATH', '')
|
|
51
|
+
|
|
52
|
+
subprocess.Popen(command, stdout=None, stderr=None, stdin=None, close_fds=True, env=env)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Generic daemon launcher for Fustor services.
|
|
4
|
+
This script is used to launch any Fustor service as a daemon.
|
|
5
|
+
It receives service parameters via command line arguments.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
import importlib
|
|
11
|
+
import argparse
|
|
12
|
+
from fustor_common.paths import get_fustor_home_dir
|
|
13
|
+
from fustor_common.logging_config import setup_logging
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
parser = argparse.ArgumentParser(description='Generic Fustor Service Daemon Launcher')
|
|
18
|
+
parser.add_argument('module_path', help='Module path to import (e.g., fustor_registry.main)')
|
|
19
|
+
parser.add_argument('app_var', help='Application variable name (e.g., app)')
|
|
20
|
+
parser.add_argument('pid_file', help='PID file name (e.g., registry.pid)')
|
|
21
|
+
parser.add_argument('log_file', help='Log file name (e.g., registry.log)')
|
|
22
|
+
parser.add_argument('display_name', help='Display name for the service')
|
|
23
|
+
parser.add_argument('port', type=int, help='Port to run the service on')
|
|
24
|
+
parser.add_argument('host', nargs='?', default='127.0.0.1', help='Host to bind the service to (default: 127.0.0.1)')
|
|
25
|
+
parser.add_argument('--reload', action='store_true', help='Enable auto-reloading for development')
|
|
26
|
+
parser.add_argument('--verbose', action='store_true', help='Enable verbose logging')
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
# Setup paths
|
|
31
|
+
HOME_FUSTOR_DIR = get_fustor_home_dir()
|
|
32
|
+
PID_FILE = os.path.join(HOME_FUSTOR_DIR, args.pid_file)
|
|
33
|
+
LOG_FILE = os.path.join(HOME_FUSTOR_DIR, args.log_file)
|
|
34
|
+
|
|
35
|
+
# Setup logging
|
|
36
|
+
log_level = "DEBUG" if args.verbose else "INFO"
|
|
37
|
+
setup_logging(
|
|
38
|
+
log_file_path=LOG_FILE,
|
|
39
|
+
base_logger_name=args.display_name.lower().replace(' ', '_') + '_daemon',
|
|
40
|
+
level=log_level,
|
|
41
|
+
console_output=False # No console output for daemon
|
|
42
|
+
)
|
|
43
|
+
logger = logging.getLogger(args.display_name.lower().replace(' ', '_') + '_daemon')
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Import the service module
|
|
47
|
+
service_module = importlib.import_module(args.module_path)
|
|
48
|
+
app = getattr(service_module, args.app_var)
|
|
49
|
+
|
|
50
|
+
# Ensure directory exists and create PID file
|
|
51
|
+
os.makedirs(HOME_FUSTOR_DIR, exist_ok=True)
|
|
52
|
+
with open(PID_FILE, 'w') as f:
|
|
53
|
+
f.write(str(os.getpid()))
|
|
54
|
+
|
|
55
|
+
print()
|
|
56
|
+
print("="*60)
|
|
57
|
+
print(f"{args.display_name} (Daemon)")
|
|
58
|
+
print(f"Web : http://{args.host}:{args.port}")
|
|
59
|
+
print("="*60)
|
|
60
|
+
print()
|
|
61
|
+
|
|
62
|
+
logger.info(f"{args.display_name} daemon starting on {args.host}:{args.port}")
|
|
63
|
+
|
|
64
|
+
# Import and run uvicorn
|
|
65
|
+
import uvicorn
|
|
66
|
+
# Configure uvicorn to use DEBUG level for access logs to reduce verbosity
|
|
67
|
+
# Need to set this after import but before run, and ensure it persists
|
|
68
|
+
uvicorn_logger = logging.getLogger("uvicorn.access")
|
|
69
|
+
uvicorn_logger.setLevel(logging.DEBUG)
|
|
70
|
+
|
|
71
|
+
uvicorn.run(
|
|
72
|
+
app,
|
|
73
|
+
host=args.host,
|
|
74
|
+
port=args.port,
|
|
75
|
+
log_config=None, # Logging handled separately
|
|
76
|
+
access_log=True,
|
|
77
|
+
reload=args.reload,
|
|
78
|
+
)
|
|
79
|
+
except KeyboardInterrupt:
|
|
80
|
+
logger.info(f"{args.display_name} daemon interrupted")
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.critical(f"{args.display_name} daemon error: {e}", exc_info=True)
|
|
83
|
+
print(f"{args.display_name} daemon error: {e}")
|
|
84
|
+
finally:
|
|
85
|
+
# Clean up PID file
|
|
86
|
+
if os.path.exists(PID_FILE):
|
|
87
|
+
os.remove(PID_FILE)
|
|
88
|
+
logger.info("PID file removed")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
main()
|
fustor_common/enums.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# src/fustor_common/logging_config.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import logging.config
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
class UvicornAccessFilter(logging.Filter):
|
|
9
|
+
"""
|
|
10
|
+
Filter to suppress uvicorn.access logs when system level is INFO,
|
|
11
|
+
but allow them when system level is DEBUG.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, normal_level: int = logging.INFO):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.normal_level = normal_level
|
|
16
|
+
|
|
17
|
+
def filter(self, record):
|
|
18
|
+
# If it's a uvicorn.access log, only allow it through if system level is DEBUG
|
|
19
|
+
if record.name.startswith('uvicorn.access'):
|
|
20
|
+
return self.normal_level <= logging.DEBUG
|
|
21
|
+
# For other logs, apply normal level filtering
|
|
22
|
+
else:
|
|
23
|
+
return record.levelno >= self.normal_level
|
|
24
|
+
|
|
25
|
+
def setup_logging(
|
|
26
|
+
log_file_path: str, # Now accepts full path
|
|
27
|
+
base_logger_name: str,
|
|
28
|
+
level: int = logging.INFO,
|
|
29
|
+
console_output: bool = True
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
通用日志配置函数。
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
log_file_path (str): 日志文件存放的完整路径。
|
|
36
|
+
base_logger_name (str): 您的应用程序的基础logger名称(例如,"fustor_agent"或"fustor_fusion")。
|
|
37
|
+
level (int): 控制台和文件处理程序的最低日志级别。
|
|
38
|
+
console_output (bool): 是否将日志输出到控制台。
|
|
39
|
+
"""
|
|
40
|
+
# 确保日志文件所在的目录存在
|
|
41
|
+
log_directory = os.path.dirname(log_file_path)
|
|
42
|
+
os.makedirs(log_directory, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
# Truncate the log file at startup
|
|
45
|
+
if os.path.exists(log_file_path):
|
|
46
|
+
try:
|
|
47
|
+
with open(log_file_path, 'w', encoding='utf8'):
|
|
48
|
+
pass # Open and immediately close to truncate
|
|
49
|
+
except IOError as e:
|
|
50
|
+
# Log the error, but don't prevent further logging
|
|
51
|
+
logging.getLogger(base_logger_name).error(f"Failed to truncate log file {log_file_path}: {e}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if isinstance(level, str):
|
|
55
|
+
numeric_level = getattr(logging, level.upper(), logging.INFO)
|
|
56
|
+
else:
|
|
57
|
+
numeric_level = level
|
|
58
|
+
|
|
59
|
+
LOGGING_CONFIG = {
|
|
60
|
+
'version': 1,
|
|
61
|
+
'disable_existing_loggers': False,
|
|
62
|
+
'formatters': {
|
|
63
|
+
'standard': {
|
|
64
|
+
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
65
|
+
'datefmt': '%Y-%m-%d %H:%M:%S'
|
|
66
|
+
},
|
|
67
|
+
'color_console': {
|
|
68
|
+
'()': 'colorlog.ColoredFormatter',
|
|
69
|
+
'format': '%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s%(reset)s',
|
|
70
|
+
'datefmt': '%Y-%m-%d %H:%M:%S',
|
|
71
|
+
'log_colors': {
|
|
72
|
+
'DEBUG': 'cyan',
|
|
73
|
+
'INFO': 'green',
|
|
74
|
+
'WARNING': 'yellow',
|
|
75
|
+
'ERROR': 'red',
|
|
76
|
+
'CRITICAL': 'bold_red',
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
'json': {
|
|
80
|
+
'()': 'pythonjsonlogger.jsonlogger.JsonFormatter',
|
|
81
|
+
'format': '%(asctime)s %(levelname)s %(name)s %(message)s'
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
'handlers': {
|
|
85
|
+
'file': {
|
|
86
|
+
'class': 'logging.handlers.RotatingFileHandler',
|
|
87
|
+
'level': logging.DEBUG, # Set to DEBUG so it can catch DEBUG level logs
|
|
88
|
+
'formatter': 'standard',
|
|
89
|
+
'filename': log_file_path,
|
|
90
|
+
'maxBytes': 10485760, # 10MB
|
|
91
|
+
'backupCount': 5,
|
|
92
|
+
'encoding': 'utf8',
|
|
93
|
+
'filters': ['uvicorn_access_filter'] # Add the custom filter
|
|
94
|
+
},
|
|
95
|
+
'console': {
|
|
96
|
+
'class': 'logging.StreamHandler',
|
|
97
|
+
'level': logging.DEBUG, # Set to DEBUG for the same reason
|
|
98
|
+
'formatter': 'color_console',
|
|
99
|
+
'stream': sys.stdout,
|
|
100
|
+
'filters': ['uvicorn_access_filter'] # Add the custom filter
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
'filters': {
|
|
104
|
+
'uvicorn_access_filter': {
|
|
105
|
+
'()': UvicornAccessFilter,
|
|
106
|
+
'normal_level': numeric_level
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
'loggers': {
|
|
110
|
+
base_logger_name: {
|
|
111
|
+
'handlers': ['file'],
|
|
112
|
+
'level': numeric_level,
|
|
113
|
+
'propagate': False
|
|
114
|
+
},
|
|
115
|
+
'uvicorn': {
|
|
116
|
+
'handlers': ['file'],
|
|
117
|
+
'level': numeric_level,
|
|
118
|
+
'propagate': False
|
|
119
|
+
},
|
|
120
|
+
'uvicorn.error': {
|
|
121
|
+
'handlers': ['file'],
|
|
122
|
+
'level': numeric_level,
|
|
123
|
+
'propagate': False
|
|
124
|
+
},
|
|
125
|
+
'uvicorn.access': {
|
|
126
|
+
'handlers': ['file'],
|
|
127
|
+
'level': logging.INFO, # Standard level for access logs
|
|
128
|
+
'propagate': False
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
'root': {
|
|
132
|
+
'handlers': ['file'],
|
|
133
|
+
'level': logging.ERROR, # Keep root at ERROR to avoid noise
|
|
134
|
+
'propagate': True, # Allow root to emit to its handlers for unhandled exceptions
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if console_output:
|
|
139
|
+
LOGGING_CONFIG['loggers'][base_logger_name]['handlers'].append('console')
|
|
140
|
+
LOGGING_CONFIG['loggers']['uvicorn']['handlers'].append('console')
|
|
141
|
+
LOGGING_CONFIG['loggers']['uvicorn.error']['handlers'].append('console')
|
|
142
|
+
LOGGING_CONFIG['loggers']['uvicorn.access']['handlers'].append('console')
|
|
143
|
+
LOGGING_CONFIG['root']['handlers'].append('console')
|
|
144
|
+
|
|
145
|
+
logging.config.dictConfig(LOGGING_CONFIG)
|
|
146
|
+
|
|
147
|
+
# --- START: Silence Third-Party Loggers (more selectively) ---
|
|
148
|
+
# Only silence if the specified level is not DEBUG, to allow debugging when needed
|
|
149
|
+
if numeric_level > logging.DEBUG:
|
|
150
|
+
third_party_loggers = [
|
|
151
|
+
'httpx', 'asyncio', 'watchdog', 'sqlalchemy',
|
|
152
|
+
'alembic', 'requests', 'urllib3', 'multipart' # Add other chatty libraries here
|
|
153
|
+
]
|
|
154
|
+
for logger_name in third_party_loggers:
|
|
155
|
+
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
|
156
|
+
# --- END: Silence Third-Party Loggers ---
|
|
157
|
+
|
|
158
|
+
# Ensure main logger is available for immediate use
|
|
159
|
+
main_logger = logging.getLogger(base_logger_name)
|
|
160
|
+
main_logger.info(f"Logging configured successfully. Level: {logging.getLevelName(numeric_level)}")
|
|
161
|
+
main_logger.debug(f"Log file: {log_file_path}")
|
fustor_common/models.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
|
2
|
+
from typing import Optional, Dict, List
|
|
3
|
+
|
|
4
|
+
class ResponseBase(BaseModel):
|
|
5
|
+
# Base model for API responses, can include common fields like status, message
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
class ApiKeyBase(BaseModel):
|
|
9
|
+
id: Optional[int] = None
|
|
10
|
+
name: str = Field(..., min_length=3, max_length=50)
|
|
11
|
+
key: Optional[str] = None
|
|
12
|
+
datastore_id: int = Field(..., description="关联的存储库ID")
|
|
13
|
+
|
|
14
|
+
class MessageResponse(BaseModel):
|
|
15
|
+
"""A simple response model for messages."""
|
|
16
|
+
message: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DatastoreBase(BaseModel):
|
|
20
|
+
id: Optional[int] = None
|
|
21
|
+
name: str = Field(..., description="存储库名称")
|
|
22
|
+
visible: bool = Field(False, description="是否对公众可见")
|
|
23
|
+
meta: Optional[Dict] = Field(None, description="存储库描述")
|
|
24
|
+
allow_concurrent_push: bool = Field(False, description="是否允许并发推送")
|
|
25
|
+
session_timeout_seconds: int = Field(30, description="会话超时秒数")
|
|
26
|
+
|
|
27
|
+
class TokenResponse(BaseModel):
|
|
28
|
+
access_token: str
|
|
29
|
+
token_type: str
|
|
30
|
+
refresh_token: str | None = None
|
|
31
|
+
|
|
32
|
+
class LogoutResponse(BaseModel):
|
|
33
|
+
detail: str = Field("注销成功", description="注销操作结果消息")
|
|
34
|
+
|
|
35
|
+
class Password(BaseModel):
|
|
36
|
+
password: str = Field(
|
|
37
|
+
...,
|
|
38
|
+
min_length=8,
|
|
39
|
+
max_length=64,
|
|
40
|
+
pattern=r'^[A-Za-z\d@$!%*#?&]+$',
|
|
41
|
+
description="8-64位字符,必须包含至少1字母和1数字,允许特殊字符 @$!%*#?&"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@field_validator('password')
|
|
45
|
+
@classmethod
|
|
46
|
+
def validate_password_chars(cls, v: str) -> str:
|
|
47
|
+
if not any(c.isalpha() for c in v):
|
|
48
|
+
raise ValueError("必须包含至少一个字母")
|
|
49
|
+
if not any(c.isdigit() for c in v):
|
|
50
|
+
raise ValueError("必须包含至少一个数字")
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
class ValidationResponse(BaseModel):
|
|
54
|
+
"""A standard response model for validation actions."""
|
|
55
|
+
success: bool
|
|
56
|
+
message: str
|
|
57
|
+
|
|
58
|
+
class CleanupResponse(BaseModel):
|
|
59
|
+
"""A standard response model for cleanup actions."""
|
|
60
|
+
message: str
|
|
61
|
+
deleted_count: int
|
|
62
|
+
deleted_ids: List[str]
|
|
63
|
+
|
|
64
|
+
class AdminCredentials(BaseModel):
|
|
65
|
+
user: str
|
|
66
|
+
passwd: str
|
|
67
|
+
|
|
68
|
+
class LoginRequest(BaseModel):
|
|
69
|
+
username: str
|
|
70
|
+
password: str
|
|
71
|
+
|
|
72
|
+
class DatastoreConfig(BaseModel):
|
|
73
|
+
datastore_id: int
|
|
74
|
+
allow_concurrent_push: bool
|
|
75
|
+
session_timeout_seconds: int
|
fustor_common/paths.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def get_fustor_home_dir() -> Path:
|
|
5
|
+
"""
|
|
6
|
+
Determines the FUSTOR home directory.
|
|
7
|
+
Checks the FUSTOR_HOME environment variable first,
|
|
8
|
+
then defaults to ~/.fustor.
|
|
9
|
+
"""
|
|
10
|
+
fustor_home = os.getenv("FUSTOR_HOME")
|
|
11
|
+
if fustor_home:
|
|
12
|
+
return Path(fustor_home).expanduser().resolve()
|
|
13
|
+
return Path.home() / ".fustor"
|
fustor_common/schemas.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fustor-common
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: Common utilities and models for Fustor services
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: colorlog>=6.10.1
|
|
8
|
+
Requires-Dist: pydantic>=2.11.7
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.3.4
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
fustor_common/__init__.py,sha256=bj3JlkXf8AGPZz4wiz26wNfyK0mfVJtMj0U1IvotxrQ,156
|
|
2
|
+
fustor_common/daemon.py,sha256=V5UlZqJl9TN_wYO21yXD_Z-4a21YECnDFibOlg7Aioc,1904
|
|
3
|
+
fustor_common/daemon_launcher.py,sha256=gcfwb_X3c-rrNGcLUlBTkYFYvKKX-QvHFS4gKwLJPaw,3536
|
|
4
|
+
fustor_common/enums.py,sha256=CyhLbVhPzonNVny-Tq-XH77SJTFh0xUT40r9eXC9g64,93
|
|
5
|
+
fustor_common/exceptions.py,sha256=jTt9pxL8kNx0997VrSc7-Wyqr8hS3F14INjTNwjgx7g,84
|
|
6
|
+
fustor_common/logging_config.py,sha256=df0RVbOeDhbnJ8iLJJ94VbZ6agh5l-S64YNHW-vzww4,6180
|
|
7
|
+
fustor_common/models.py,sha256=LV3ydnXFqDOOBWKMkrORhFwYKQRdxHjwTuORmJ-5Ltw,2350
|
|
8
|
+
fustor_common/paths.py,sha256=SPoh_UsT6p65fl1DjLvLMfqtmEB40Z0rUFKYvmpUM5E,369
|
|
9
|
+
fustor_common/schemas.py,sha256=bEwg0MFGYqSGGAQqRPGqXBUP1rM24vt_UhfQtHxHYv8,240
|
|
10
|
+
fustor_common-0.1.8.dist-info/METADATA,sha256=Ol88cGXPHgyrz7p-Mqqw4l7ikGUvnWoPfOAO8wi0qcQ,266
|
|
11
|
+
fustor_common-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
fustor_common-0.1.8.dist-info/top_level.txt,sha256=eg0-4Mw7UcXKo62TbE1Mu1yU9IO9-WKR7HDmqjqH_K8,14
|
|
13
|
+
fustor_common-0.1.8.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fustor_common
|