ghoshell-common 0.4.0.dev0__tar.gz → 0.4.0.dev1__tar.gz
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.
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/PKG-INFO +2 -1
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/pyproject.toml +2 -1
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/cli/__init__.py +0 -0
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/cli/main.py +34 -0
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/__init__.py +41 -0
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/assets.py +162 -0
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/configs.py +200 -0
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/logger.py +239 -0
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/storage.py +170 -0
- ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/workspace.py +88 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/LICENSE +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/README.md +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/__init__.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/entity.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/__init__.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/code_analyser.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/coding.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/dictionary.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/files.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/hashes.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/io.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/modules.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/openai.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/string.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/timeutils.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/toml.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/trans.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/tree_sitter.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/yaml.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/identifier.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/prompter.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_code_analyser.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_files_helpers.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_get_interface.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_helpers.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_modules_helper.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_timeleft.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_toml.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_tree_sitter.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/test_entity.py +0 -0
- {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/test_prompter.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ghoshell-common
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev1
|
|
4
4
|
Summary: common library for GhostInShells project
|
|
5
5
|
Author-Email: thirdgerb <thirdgerb@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: click>=8.3.0
|
|
8
9
|
Requires-Dist: ghoshell-container>=0.3.0.dev1
|
|
9
10
|
Requires-Dist: pydantic<3.0.0,>=2.7.0
|
|
10
11
|
Requires-Dist: pyyaml<7.0.0,>=6.0.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ghoshell-common"
|
|
3
|
-
version = "0.4.0-
|
|
3
|
+
version = "0.4.0-dev1"
|
|
4
4
|
description = "common library for GhostInShells project"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "thirdgerb", email = "thirdgerb@gmail.com" },
|
|
@@ -8,6 +8,7 @@ authors = [
|
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
dependencies = [
|
|
11
|
+
"click>=8.3.0",
|
|
11
12
|
"ghoshell-container>=0.3.0.dev1",
|
|
12
13
|
"pydantic<3.0.0,>=2.7.0",
|
|
13
14
|
"pyyaml<7.0.0,>=6.0.1",
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def main():
|
|
7
|
+
"""
|
|
8
|
+
ghoshell cli main group
|
|
9
|
+
"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@main.command("help")
|
|
14
|
+
def main_help():
|
|
15
|
+
"""Print this help message."""
|
|
16
|
+
_get_command_line_as_string()
|
|
17
|
+
|
|
18
|
+
assert len(sys.argv) == 2 # This is always true, but let's assert anyway.
|
|
19
|
+
# Pretend user typed 'streamlit --help' instead of 'streamlit help'.
|
|
20
|
+
sys.argv[1] = "--help"
|
|
21
|
+
main(prog_name="main")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_command_line_as_string() -> str | None:
|
|
25
|
+
"""Print this help message."""
|
|
26
|
+
import sys
|
|
27
|
+
import subprocess
|
|
28
|
+
parent = click.get_current_context().parent
|
|
29
|
+
if parent is None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
cmd_line_as_list = [parent.command_path]
|
|
33
|
+
cmd_line_as_list.extend(sys.argv[1:])
|
|
34
|
+
return subprocess.list2cmdline(cmd_line_as_list)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from ghoshell_common.contracts.storage import (
|
|
2
|
+
Storage, FileStorage,
|
|
3
|
+
FileStorageProvider,
|
|
4
|
+
DefaultFileStorage,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from ghoshell_common.contracts.workspace import (
|
|
8
|
+
Workspace,
|
|
9
|
+
LocalWorkspaceProvider,
|
|
10
|
+
LocalWorkspace,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from ghoshell_common.contracts.configs import (
|
|
14
|
+
ConfigType, Configs,
|
|
15
|
+
YamlConfig,
|
|
16
|
+
DefaultConfigs,
|
|
17
|
+
WorkspaceConfigs, WorkspaceConfigsProvider
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ghoshell_common.contracts.logger import (
|
|
21
|
+
LoggerItf,
|
|
22
|
+
LoggerProvider, WorkspaceLoggerProvider,
|
|
23
|
+
get_console_logger, config_logger_from_yaml,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from ghoshell_common.contracts.assets import (
|
|
27
|
+
FileAsset, FileAssetRepo,
|
|
28
|
+
ImageAssetRepo, AudioAssetRepo,
|
|
29
|
+
WorkspaceAssetsRepoProvider,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def workspace_providers(workspace_dir: str = "", stub_dir: str | None = None):
|
|
34
|
+
"""
|
|
35
|
+
default providers.
|
|
36
|
+
"""
|
|
37
|
+
yield LocalWorkspaceProvider(workspace_dir, stub_dir)
|
|
38
|
+
yield WorkspaceConfigsProvider()
|
|
39
|
+
yield WorkspaceLoggerProvider("ghoshell")
|
|
40
|
+
yield WorkspaceAssetsRepoProvider(ImageAssetRepo, "images")
|
|
41
|
+
yield WorkspaceAssetsRepoProvider(AudioAssetRepo, "audio")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, Tuple, Union, Type
|
|
3
|
+
from mimetypes import guess_type
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
from ghoshell_common.contracts.storage import Storage
|
|
6
|
+
from ghoshell_common.helpers import uuid, yaml_pretty_dump
|
|
7
|
+
from ghoshell_container import Provider, IoCContainer
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'FileAsset', 'FileAssetRepo',
|
|
12
|
+
'AudioAssetRepo', 'ImageAssetRepo',
|
|
13
|
+
'StorageFileAssetRepo',
|
|
14
|
+
'WorkspaceAssetsRepoProvider',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileAsset(BaseModel):
|
|
19
|
+
fileid: str = Field(default_factory=uuid, description="ID of the file.")
|
|
20
|
+
filename: str = Field(description="The file name of the file.")
|
|
21
|
+
description: str = Field(default="", description="The description of the file.")
|
|
22
|
+
filetype: str = Field(default="", description="The file type of the file.")
|
|
23
|
+
summary: str = Field(default="", description="The text summary of the file.")
|
|
24
|
+
url: Optional[str] = Field(default=None, description="The URL of the file.")
|
|
25
|
+
|
|
26
|
+
def get_format(self) -> str:
|
|
27
|
+
return self.filetype.split("/")[-1]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FileAssetRepo(ABC):
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def new_fileinfo(
|
|
34
|
+
cls,
|
|
35
|
+
fileid: str,
|
|
36
|
+
filename: str,
|
|
37
|
+
description: str = "",
|
|
38
|
+
filetype: Optional[str] = None,
|
|
39
|
+
) -> FileAsset:
|
|
40
|
+
if filetype is None:
|
|
41
|
+
filetype, _ = guess_type(filename)
|
|
42
|
+
fileinfo = FileAsset(
|
|
43
|
+
fileid=fileid,
|
|
44
|
+
filename=filename,
|
|
45
|
+
description=description,
|
|
46
|
+
filetype=filetype,
|
|
47
|
+
)
|
|
48
|
+
return fileinfo
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def save(self, file: FileAsset, binary: Optional[bytes]) -> str:
|
|
52
|
+
"""
|
|
53
|
+
save file info and binary data to storage
|
|
54
|
+
:param file: the file info
|
|
55
|
+
:param binary: binary data of the file. if None, the url must be provided
|
|
56
|
+
:return: relative file path of the saved file
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_binary(self, filename: str) -> Optional[bytes]:
|
|
62
|
+
"""
|
|
63
|
+
get binary data of the file
|
|
64
|
+
:param filename: the relative filename of the file
|
|
65
|
+
:return: binary data of the file, None if binary data is not available
|
|
66
|
+
"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def get_fileinfo(self, fileid: str) -> Optional[FileAsset]:
|
|
71
|
+
"""
|
|
72
|
+
get file info from storage
|
|
73
|
+
:param fileid: the file id
|
|
74
|
+
:return: None if no file info is available
|
|
75
|
+
"""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def has_binary(self, fileid: str) -> bool:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def get_file_and_binary_by_id(self, fileid: str) -> Tuple[Union[FileAsset, None], Union[bytes, None]]:
|
|
83
|
+
"""
|
|
84
|
+
get binary data by file id
|
|
85
|
+
:param fileid: the file info id.
|
|
86
|
+
:return: file info and binary data, if binary data is None, means the file has url.
|
|
87
|
+
"""
|
|
88
|
+
file_info = self.get_fileinfo(fileid)
|
|
89
|
+
if file_info is None:
|
|
90
|
+
return None, None
|
|
91
|
+
return file_info, self.get_binary(file_info.filename)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ImageAssetRepo(FileAssetRepo, ABC):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class AudioAssetRepo(FileAssetRepo, ABC):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class StorageFileAssetRepo(FileAssetRepo):
|
|
103
|
+
"""
|
|
104
|
+
simple implementation of FileAssets
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, storage: Storage):
|
|
108
|
+
self._storage = storage
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _get_fileinfo_filename(fileid: str) -> str:
|
|
112
|
+
return f"{fileid}.yml"
|
|
113
|
+
|
|
114
|
+
def save(self, file: FileAsset, binary: Optional[bytes]) -> str:
|
|
115
|
+
if binary is None and file.url is None:
|
|
116
|
+
raise AttributeError("failed to save image: binary is None and image info is not from url.")
|
|
117
|
+
fileinfo_filename = self._get_fileinfo_filename(file.fileid)
|
|
118
|
+
data = file.model_dump(exclude_none=True)
|
|
119
|
+
content = yaml_pretty_dump(data)
|
|
120
|
+
self._storage.put(fileinfo_filename, content.encode())
|
|
121
|
+
if binary:
|
|
122
|
+
self._storage.put(file.filename, binary)
|
|
123
|
+
return file.filename
|
|
124
|
+
|
|
125
|
+
def get_binary(self, filename: str) -> Optional[bytes]:
|
|
126
|
+
if self._storage.exists(filename):
|
|
127
|
+
return self._storage.get(filename)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def has_binary(self, fileid: str) -> bool:
|
|
131
|
+
fileinfo = self.get_fileinfo(fileid)
|
|
132
|
+
if fileinfo is None:
|
|
133
|
+
return False
|
|
134
|
+
filename = fileinfo.filename
|
|
135
|
+
return self._storage.exists(filename)
|
|
136
|
+
|
|
137
|
+
def get_fileinfo(self, fileid: str) -> Optional[FileAsset]:
|
|
138
|
+
fileinfo_filename = self._get_fileinfo_filename(fileid)
|
|
139
|
+
if not self._storage.exists(fileinfo_filename):
|
|
140
|
+
return None
|
|
141
|
+
content = self._storage.get(fileinfo_filename)
|
|
142
|
+
data = yaml.safe_load(content)
|
|
143
|
+
return FileAsset(**data)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class WorkspaceAssetsRepoProvider(Provider):
|
|
147
|
+
|
|
148
|
+
def __init__(self, assets_type: Type[FileAssetRepo], relative_path: str):
|
|
149
|
+
self._assets_type = assets_type
|
|
150
|
+
self._relative_path = relative_path
|
|
151
|
+
|
|
152
|
+
def singleton(self) -> bool:
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def contract(self) -> Type[FileAssetRepo]:
|
|
156
|
+
return self._assets_type
|
|
157
|
+
|
|
158
|
+
def factory(self, con: IoCContainer) -> FileAssetRepo:
|
|
159
|
+
from ghoshell_common.contracts.workspace import Workspace
|
|
160
|
+
ws = con.force_fetch(Workspace)
|
|
161
|
+
repo = StorageFileAssetRepo(ws.assets().sub_storage(self._relative_path))
|
|
162
|
+
return repo
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import ClassVar, TypeVar, Type, Optional, List
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from ghoshell_common.helpers import generate_import_path
|
|
7
|
+
from ghoshell_common.contracts.storage import FileStorage
|
|
8
|
+
from ghoshell_container import Container, Provider
|
|
9
|
+
from os.path import join, abspath, exists
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'ConfigType', 'Configs', 'YamlConfig',
|
|
13
|
+
'WorkspaceConfigs', 'WorkspaceConfigsProvider',
|
|
14
|
+
'DefaultConfigs',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConfigType(ABC):
|
|
19
|
+
"""
|
|
20
|
+
从 workspace 中获取配置文件, 基于 Pydantic + Yaml 或 toml 定义配置.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def conf_path(cls) -> str:
|
|
26
|
+
"""
|
|
27
|
+
当前 Config 存储时对于 configs 目录的相对路径.
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def unmarshal(cls, content: bytes) -> Self:
|
|
34
|
+
"""
|
|
35
|
+
反序列化存储数据的方法.
|
|
36
|
+
"""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
def marshal(self) -> bytes:
|
|
40
|
+
"""
|
|
41
|
+
生成自己存储数据的方法.
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
CONF_TYPE = TypeVar('CONF_TYPE', bound=ConfigType)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Configs(ABC):
|
|
50
|
+
"""
|
|
51
|
+
存储所有 Config 对象的仓库.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def get(self, conf_type: Type[CONF_TYPE], relative_path: Optional[str] = None) -> CONF_TYPE:
|
|
56
|
+
"""
|
|
57
|
+
从仓库中读取一个配置对象.
|
|
58
|
+
:param conf_type: C 类型配置对象的类.
|
|
59
|
+
:param relative_path: 默认不需要填. 如果读取路径不是 C 类型默认的, 才需要传入.
|
|
60
|
+
:return: C 类型的实例.
|
|
61
|
+
:exception: FileNotFoundError
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def get_or_create(self, conf: CONF_TYPE) -> CONF_TYPE:
|
|
67
|
+
"""
|
|
68
|
+
如果配置对象不存在, 则创建一个.
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def save(self, conf: ConfigType, relative_path: Optional[str] = None) -> None:
|
|
74
|
+
"""
|
|
75
|
+
保存一个 Config 对象.
|
|
76
|
+
:param conf: the conf object
|
|
77
|
+
:param relative_path: if pass, override the conf_type default path.
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class YamlConfig(ConfigType, BaseModel):
|
|
83
|
+
"""
|
|
84
|
+
基于 Yaml + BaseModel 实现的配置文件.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
relative_path: ClassVar[str]
|
|
88
|
+
"""定义默认的相对存储路径. 通常存储在 workspace/configs/relative_path"""
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def conf_path(cls) -> str:
|
|
92
|
+
return cls.relative_path
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def unmarshal(cls, content: str) -> "ConfigType":
|
|
96
|
+
value = yaml.safe_load(content)
|
|
97
|
+
return cls(**value)
|
|
98
|
+
|
|
99
|
+
def marshal(self) -> bytes:
|
|
100
|
+
value = self.model_dump()
|
|
101
|
+
comment = f"# from class: {generate_import_path(self.__class__)}"
|
|
102
|
+
result = yaml.safe_dump(value)
|
|
103
|
+
return "\n".join([comment, result]).encode()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BasicConfigs(Configs, ABC):
|
|
107
|
+
"""
|
|
108
|
+
A Configs(repository) based on Storage, no matter what the Storage is.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def get(self, conf_type: Type[CONF_TYPE], relative_path: Optional[str] = None) -> CONF_TYPE:
|
|
112
|
+
path = conf_type.conf_path()
|
|
113
|
+
relative_path = relative_path if relative_path else path
|
|
114
|
+
content = self._get(relative_path)
|
|
115
|
+
return conf_type.unmarshal(content)
|
|
116
|
+
|
|
117
|
+
def get_or_create(self, conf: CONF_TYPE) -> CONF_TYPE:
|
|
118
|
+
path = conf.conf_path()
|
|
119
|
+
if not self._exists(path):
|
|
120
|
+
self._put(path, conf.marshal())
|
|
121
|
+
return conf
|
|
122
|
+
return self.get(type(conf))
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def _get(self, relative_path: str) -> bytes:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def _put(self, relative_path: str, content: bytes) -> None:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def _exists(self, relative_path: str) -> bool:
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
def save(self, conf: ConfigType, relative_path: Optional[str] = None) -> None:
|
|
137
|
+
marshaled = conf.marshal()
|
|
138
|
+
relative_path = relative_path if relative_path else conf.conf_path()
|
|
139
|
+
self._put(relative_path, marshaled)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DefaultConfigs(BasicConfigs):
|
|
143
|
+
"""
|
|
144
|
+
A Configs(repository) based on Storage, no matter what the Storage is.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, configs_dir: str):
|
|
148
|
+
self._configs_dir = configs_dir
|
|
149
|
+
|
|
150
|
+
def _get(self, relative_path: str) -> bytes:
|
|
151
|
+
abs_path = abspath(join(self._configs_dir, relative_path))
|
|
152
|
+
with open(abs_path, 'rb') as f:
|
|
153
|
+
return f.read()
|
|
154
|
+
|
|
155
|
+
def _put(self, relative_path: str, content: bytes) -> None:
|
|
156
|
+
abs_path = abspath(join(self._configs_dir, relative_path))
|
|
157
|
+
with open(abs_path, 'wb') as f:
|
|
158
|
+
f.write(content)
|
|
159
|
+
|
|
160
|
+
def _exists(self, relative_path: str) -> bool:
|
|
161
|
+
abs_path = abspath(join(self._configs_dir, relative_path))
|
|
162
|
+
return exists(abs_path)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class WorkspaceConfigs(BasicConfigs):
|
|
166
|
+
|
|
167
|
+
def __init__(self, storage: FileStorage):
|
|
168
|
+
self._storage = storage
|
|
169
|
+
|
|
170
|
+
def _get(self, relative_path: str) -> bytes:
|
|
171
|
+
return self._storage.get(relative_path)
|
|
172
|
+
|
|
173
|
+
def _put(self, relative_path: str, content: bytes) -> None:
|
|
174
|
+
self._storage.put(relative_path, content)
|
|
175
|
+
|
|
176
|
+
def _exists(self, relative_path: str) -> bool:
|
|
177
|
+
return self._storage.exists(relative_path)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class WorkspaceConfigsProvider(Provider[Configs]):
|
|
181
|
+
"""
|
|
182
|
+
默认的 configs 实现.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(self, default_configs: List[ConfigType] | None = None):
|
|
186
|
+
self._create_default_configs = default_configs
|
|
187
|
+
|
|
188
|
+
def singleton(self) -> bool:
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
def factory(self, con: Container) -> Optional[Configs]:
|
|
192
|
+
from ghoshell_common.contracts.workspace import Workspace
|
|
193
|
+
ws = con.force_fetch(Workspace)
|
|
194
|
+
|
|
195
|
+
configs = WorkspaceConfigs(ws.configs())
|
|
196
|
+
# 创建默认的配置文件
|
|
197
|
+
if self._create_default_configs:
|
|
198
|
+
for default_config in self._create_default_configs:
|
|
199
|
+
configs.get_or_create(default_config)
|
|
200
|
+
return configs
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import warnings
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import Optional, Protocol
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from logging.config import dictConfig
|
|
7
|
+
from ghoshell_container import Container, Provider, INSTANCE, IoCContainer
|
|
8
|
+
from os import path
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'LoggerItf',
|
|
13
|
+
'WorkspaceLoggerProvider', 'LoggerProvider',
|
|
14
|
+
'config_workspace_logger', 'get_console_logger', 'config_logger_from_yaml', 'get_logger_with_extra',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoggerItf(Protocol):
|
|
19
|
+
"""
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def debug(self, msg, *args, **kwargs):
|
|
24
|
+
"""
|
|
25
|
+
Log 'msg % args' with severity 'DEBUG'.
|
|
26
|
+
|
|
27
|
+
To pass exception information, use the keyword argument exc_info with
|
|
28
|
+
a true value, e.g.
|
|
29
|
+
|
|
30
|
+
logger.debug("Houston, we have a %s", "thorny problem", exc_info=True)
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def info(self, msg, *args, **kwargs):
|
|
36
|
+
"""
|
|
37
|
+
Log 'msg % args' with severity 'INFO'.
|
|
38
|
+
|
|
39
|
+
To pass exception information, use the keyword argument exc_info with
|
|
40
|
+
a true value, e.g.
|
|
41
|
+
|
|
42
|
+
logger.debug("Houston, we have a %s", "notable problem", exc_info=True)
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def warning(self, msg, *args, **kwargs):
|
|
48
|
+
"""
|
|
49
|
+
Log 'msg % args' with severity 'WARNING'.
|
|
50
|
+
|
|
51
|
+
To pass exception information, use the keyword argument exc_info with
|
|
52
|
+
a true value, e.g.
|
|
53
|
+
|
|
54
|
+
logger.warning("Houston, we have a %s", "bit of a problem", exc_info=True)
|
|
55
|
+
"""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def error(self, msg, *args, **kwargs):
|
|
60
|
+
"""
|
|
61
|
+
Log 'msg % args' with severity 'ERROR'.
|
|
62
|
+
|
|
63
|
+
To pass exception information, use the keyword argument exc_info with
|
|
64
|
+
a true value, e.g.
|
|
65
|
+
|
|
66
|
+
logger.error("Houston, we have a %s", "major problem", exc_info=True)
|
|
67
|
+
"""
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def exception(self, msg, *args, exc_info=True, **kwargs):
|
|
72
|
+
"""
|
|
73
|
+
Convenience method for logging an ERROR with exception information.
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def critical(self, msg, *args, **kwargs):
|
|
79
|
+
"""
|
|
80
|
+
Log 'msg % args' with severity 'CRITICAL'.
|
|
81
|
+
|
|
82
|
+
To pass exception information, use the keyword argument exc_info with
|
|
83
|
+
a true value, e.g.
|
|
84
|
+
|
|
85
|
+
logger.critical("Houston, we have a %s", "major disaster", exc_info=True)
|
|
86
|
+
"""
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def log(self, level, msg, *args, **kwargs):
|
|
91
|
+
"""
|
|
92
|
+
Log 'msg % args' with the integer severity 'level'.
|
|
93
|
+
|
|
94
|
+
To pass exception information, use the keyword argument exc_info with
|
|
95
|
+
a true value, e.g.
|
|
96
|
+
|
|
97
|
+
logger.log(level, "We have a %s", "mysterious problem", exc_info=True)
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PleshakovFormatter(logging.Formatter):
|
|
103
|
+
# copy from
|
|
104
|
+
# https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output
|
|
105
|
+
grey = "\x1b[37;20m"
|
|
106
|
+
yellow = "\x1b[33;20m"
|
|
107
|
+
red = "\x1b[31;20m"
|
|
108
|
+
green = "\x1b[32;20m"
|
|
109
|
+
bold_red = "\x1b[31;1m"
|
|
110
|
+
reset = "\x1b[0m"
|
|
111
|
+
format = "%(asctime)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
|
|
112
|
+
|
|
113
|
+
FORMATS = {
|
|
114
|
+
logging.DEBUG: grey + format + reset,
|
|
115
|
+
logging.INFO: green + format + reset,
|
|
116
|
+
logging.WARNING: yellow + format + reset,
|
|
117
|
+
logging.ERROR: red + format + reset,
|
|
118
|
+
logging.CRITICAL: bold_red + format + reset
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
def format(self, record):
|
|
122
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
|
123
|
+
formatter = logging.Formatter(log_fmt)
|
|
124
|
+
return formatter.format(record)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class FakeLogger(LoggerItf):
|
|
128
|
+
def debug(self, msg, *args, **kwargs):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def info(self, msg, *args, **kwargs):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
def warning(self, msg, *args, **kwargs):
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
def error(self, msg, *args, **kwargs):
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
def exception(self, msg, *args, exc_info=True, **kwargs):
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def critical(self, msg, *args, **kwargs):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def log(self, level, msg, *args, **kwargs):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_logger_with_extra(name: Optional[str] = None, extra: Optional[dict] = None) -> LoggerItf:
|
|
151
|
+
return logging.LoggerAdapter(getLogger(name), extra=extra)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def config_logger_from_yaml(yaml_conf_path: str) -> None:
|
|
155
|
+
"""
|
|
156
|
+
configurate logging by yaml config
|
|
157
|
+
:param yaml_conf_path: absolute path of yaml config file
|
|
158
|
+
"""
|
|
159
|
+
if not path.exists(yaml_conf_path):
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
with open(yaml_conf_path) as f:
|
|
163
|
+
content = f.read()
|
|
164
|
+
data = yaml.safe_load(content)
|
|
165
|
+
dictConfig(data)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def config_workspace_logger(workspace_dir: str) -> None:
|
|
169
|
+
from os.path import join, exists
|
|
170
|
+
logger_path = join(workspace_dir, "logging.yml")
|
|
171
|
+
if not exists(logger_path):
|
|
172
|
+
warnings.warn("Coco logger not found at '{}'".format(logger_path))
|
|
173
|
+
else:
|
|
174
|
+
config_logger_from_yaml(logger_path)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_console_logger(
|
|
178
|
+
name: str,
|
|
179
|
+
extra: Optional[dict] = None,
|
|
180
|
+
debug: bool = False,
|
|
181
|
+
) -> LoggerItf:
|
|
182
|
+
logger = getLogger(name)
|
|
183
|
+
if not logger.hasHandlers():
|
|
184
|
+
_console_handler = logging.StreamHandler()
|
|
185
|
+
_console_formatter = PleshakovFormatter()
|
|
186
|
+
_console_handler.setFormatter(_console_formatter)
|
|
187
|
+
logger.addHandler(_console_handler)
|
|
188
|
+
|
|
189
|
+
if debug:
|
|
190
|
+
logger.setLevel(logging.DEBUG)
|
|
191
|
+
return logging.LoggerAdapter(logger, extra=extra)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class LoggerProvider(Provider[LoggerItf]):
|
|
195
|
+
"""
|
|
196
|
+
注册日志服务, 方便从容器中获取日志实例.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
name: str = "ghoshell",
|
|
202
|
+
level: Optional[int] = None,
|
|
203
|
+
extra: Optional[dict] = None,
|
|
204
|
+
):
|
|
205
|
+
self.name = name
|
|
206
|
+
self.level = level
|
|
207
|
+
self.extra = extra
|
|
208
|
+
|
|
209
|
+
def singleton(self) -> bool:
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
def contract(self):
|
|
213
|
+
return LoggerItf
|
|
214
|
+
|
|
215
|
+
def factory(self, con: Container) -> Optional[LoggerItf]:
|
|
216
|
+
logger = logging.getLogger(self.name)
|
|
217
|
+
return logging.LoggerAdapter(logger, self.extra)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class WorkspaceLoggerProvider(Provider[LoggerItf]):
|
|
221
|
+
"""
|
|
222
|
+
logger from workspace.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(self, name: str):
|
|
226
|
+
self.name = name
|
|
227
|
+
|
|
228
|
+
def singleton(self) -> bool:
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
def factory(self, con: IoCContainer) -> INSTANCE:
|
|
232
|
+
from ghoshell_common.contracts.workspace import Workspace
|
|
233
|
+
ws = con.force_fetch(Workspace)
|
|
234
|
+
if not ws.configs().exists("logging.yml"):
|
|
235
|
+
return get_console_logger(self.name)
|
|
236
|
+
else:
|
|
237
|
+
logging_config_path = ws.configs().abspath().join("logging.yml")
|
|
238
|
+
config_logger_from_yaml(logging_config_path)
|
|
239
|
+
return logging.getLogger(self.name)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Optional, Iterable, Protocol
|
|
3
|
+
import os
|
|
4
|
+
from ghoshell_container import Container, Provider
|
|
5
|
+
import fnmatch
|
|
6
|
+
|
|
7
|
+
__all__ = ['Storage', 'FileStorage', 'DefaultFileStorage', 'FileStorageProvider']
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Storage(Protocol):
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def sub_storage(self, relative_path: str) -> "Storage":
|
|
14
|
+
"""
|
|
15
|
+
生成一个次级目录下的 storage.
|
|
16
|
+
:param relative_path:
|
|
17
|
+
:return:
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def get(self, file_path: str) -> bytes:
|
|
23
|
+
"""
|
|
24
|
+
获取一个 Storage 路径下一个文件的内容.
|
|
25
|
+
:param file_path: storage 下的一个相对路径.
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def remove(self, file_path: str) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def exists(self, file_path: str) -> bool:
|
|
35
|
+
"""
|
|
36
|
+
if the object exists
|
|
37
|
+
:param file_path: file_path or directory path
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def put(self, file_path: str, content: bytes) -> None:
|
|
43
|
+
"""
|
|
44
|
+
保存一个文件的内容到 file_path .
|
|
45
|
+
:param file_path: storage 下的一个相对路径.
|
|
46
|
+
:param content: 文件的内容.
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def dir(self, prefix_dir: str, recursive: bool, patten: Optional[str] = None) -> Iterable[str]:
|
|
52
|
+
"""
|
|
53
|
+
遍历一个路径下的文件, 返回相对的文件名.
|
|
54
|
+
:param prefix_dir: 目录的相对路径位置.
|
|
55
|
+
:param recursive: 是否递归查找.
|
|
56
|
+
:param patten: 文件的正则规范.
|
|
57
|
+
:return: 多个文件路径名.
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FileStorage(Storage, Protocol):
|
|
63
|
+
"""
|
|
64
|
+
Storage Based on FileSystem.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def abspath(self) -> str:
|
|
69
|
+
"""
|
|
70
|
+
storage root directory's absolute path
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def sub_storage(self, relative_path: str) -> "FileStorage":
|
|
76
|
+
"""
|
|
77
|
+
FileStorage's sub storage is still FileStorage
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DefaultFileStorage(FileStorage):
|
|
83
|
+
"""
|
|
84
|
+
FileStorage implementation based on python filesystem.
|
|
85
|
+
Simplest implementation.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, dir_: str):
|
|
89
|
+
self._dir: str = os.path.abspath(dir_)
|
|
90
|
+
|
|
91
|
+
def abspath(self) -> str:
|
|
92
|
+
return self._dir
|
|
93
|
+
|
|
94
|
+
def get(self, file_path: str) -> bytes:
|
|
95
|
+
file_path = self._join_file_path(file_path)
|
|
96
|
+
with open(file_path, 'rb') as f:
|
|
97
|
+
return f.read()
|
|
98
|
+
|
|
99
|
+
def remove(self, file_path: str) -> None:
|
|
100
|
+
file_path = self._join_file_path(file_path)
|
|
101
|
+
os.remove(file_path)
|
|
102
|
+
|
|
103
|
+
def exists(self, file_path: str) -> bool:
|
|
104
|
+
file_path = self._join_file_path(file_path)
|
|
105
|
+
return os.path.exists(file_path)
|
|
106
|
+
|
|
107
|
+
def _join_file_path(self, path: str) -> str:
|
|
108
|
+
file_path = os.path.join(self._dir, path)
|
|
109
|
+
file_path = os.path.abspath(file_path)
|
|
110
|
+
if not file_path.startswith(self._dir):
|
|
111
|
+
raise FileNotFoundError(f"file path {path} is not allowed")
|
|
112
|
+
return file_path
|
|
113
|
+
|
|
114
|
+
def put(self, file_path: str, content: bytes) -> None:
|
|
115
|
+
file_path = self._join_file_path(file_path)
|
|
116
|
+
if not file_path.startswith(self._dir):
|
|
117
|
+
raise FileNotFoundError(f"file path {file_path} is not allowed")
|
|
118
|
+
file_dir = os.path.dirname(file_path)
|
|
119
|
+
if not os.path.exists(file_dir):
|
|
120
|
+
os.makedirs(file_dir)
|
|
121
|
+
with open(file_path, 'wb') as f:
|
|
122
|
+
f.write(content)
|
|
123
|
+
|
|
124
|
+
def sub_storage(self, relative_path: str) -> "FileStorage":
|
|
125
|
+
if not relative_path:
|
|
126
|
+
return self
|
|
127
|
+
dir_path = self._join_file_path(relative_path)
|
|
128
|
+
return DefaultFileStorage(dir_path)
|
|
129
|
+
|
|
130
|
+
def dir(self, prefix_dir: str, recursive: bool, patten: Optional[str] = None) -> Iterable[str]:
|
|
131
|
+
dir_path = self._join_file_path(prefix_dir)
|
|
132
|
+
for root, ds, fs in os.walk(dir_path):
|
|
133
|
+
# 遍历单层的文件名.
|
|
134
|
+
for filename in fs:
|
|
135
|
+
if self._match_file_pattern(filename, patten):
|
|
136
|
+
yield filename
|
|
137
|
+
|
|
138
|
+
# 深入遍历目录.
|
|
139
|
+
if recursive and ds:
|
|
140
|
+
for _dir in ds:
|
|
141
|
+
sub_dir_iterator = self.dir(_dir, recursive, patten)
|
|
142
|
+
for filename in sub_dir_iterator:
|
|
143
|
+
yield filename
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _match_file_pattern(filename: str, pattern: Optional[str]) -> bool:
|
|
147
|
+
if pattern is None:
|
|
148
|
+
return True
|
|
149
|
+
matched = True
|
|
150
|
+
if pattern.startswith('!'):
|
|
151
|
+
matched = False
|
|
152
|
+
pattern = pattern[1:]
|
|
153
|
+
if pattern and fnmatch.fnmatch(filename, pattern):
|
|
154
|
+
return matched
|
|
155
|
+
return not matched
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class FileStorageProvider(Provider[FileStorage]):
|
|
159
|
+
|
|
160
|
+
def __init__(self, dir_: str):
|
|
161
|
+
self._dir: str = dir_
|
|
162
|
+
|
|
163
|
+
def singleton(self) -> bool:
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
def aliases(self) -> Iterable[Storage]:
|
|
167
|
+
yield Storage
|
|
168
|
+
|
|
169
|
+
def factory(self, con: Container) -> Optional[Storage]:
|
|
170
|
+
return DefaultFileStorage(self._dir)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ghoshell_common.contracts.storage import FileStorage, DefaultFileStorage
|
|
6
|
+
from ghoshell_container import Container, Provider
|
|
7
|
+
from os.path import abspath
|
|
8
|
+
import shutil
|
|
9
|
+
|
|
10
|
+
__all__ = ['Workspace', 'LocalWorkspace', 'LocalWorkspaceProvider']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Workspace(ABC):
|
|
14
|
+
"""
|
|
15
|
+
workspace 目录文件管理.
|
|
16
|
+
用于管理一个项目的本地文件存储.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def root(self) -> FileStorage:
|
|
21
|
+
"""
|
|
22
|
+
workspace 根 storage.
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def configs(self) -> FileStorage:
|
|
28
|
+
"""
|
|
29
|
+
配置文件存储路径.
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def runtime(self) -> FileStorage:
|
|
35
|
+
"""
|
|
36
|
+
运行时数据存储路径.
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def assets(self) -> FileStorage:
|
|
42
|
+
"""
|
|
43
|
+
数据资产存储路径.
|
|
44
|
+
"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LocalWorkspace(Workspace):
|
|
49
|
+
|
|
50
|
+
def __init__(self, workspace_dir: str):
|
|
51
|
+
workspace_dir = abspath(workspace_dir)
|
|
52
|
+
self._ws_root_dir = workspace_dir
|
|
53
|
+
self._root = DefaultFileStorage(workspace_dir)
|
|
54
|
+
|
|
55
|
+
def root(self) -> FileStorage:
|
|
56
|
+
return self._root
|
|
57
|
+
|
|
58
|
+
def configs(self) -> FileStorage:
|
|
59
|
+
return self._root.sub_storage("configs")
|
|
60
|
+
|
|
61
|
+
def runtime(self) -> FileStorage:
|
|
62
|
+
return self._root.sub_storage("runtime")
|
|
63
|
+
|
|
64
|
+
def assets(self) -> FileStorage:
|
|
65
|
+
return self._root.sub_storage("assets")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class LocalWorkspaceProvider(Provider[Workspace]):
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
workspace_dir: str = "",
|
|
73
|
+
stub_dir: Optional[str] = None,
|
|
74
|
+
):
|
|
75
|
+
if workspace_dir == "":
|
|
76
|
+
# 使用脚本运行的路径作为 workspace.
|
|
77
|
+
workspace_dir = os.path.join(abspath(os.getcwd()), ".ghoshell_ws/")
|
|
78
|
+
self._ws_dir = abspath(workspace_dir)
|
|
79
|
+
self._stub_dir = abspath(stub_dir) if stub_dir else None
|
|
80
|
+
|
|
81
|
+
def singleton(self) -> bool:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def factory(self, con: Container) -> Optional[Workspace]:
|
|
85
|
+
if self._stub_dir and not os.path.exists(self._stub_dir):
|
|
86
|
+
os.makedirs(self._stub_dir)
|
|
87
|
+
shutil.copytree(self._stub_dir, self._ws_dir)
|
|
88
|
+
return LocalWorkspace(self._ws_dir)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/coding.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/dictionary.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/files.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/hashes.py
RENAMED
|
File without changes
|
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/modules.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/openai.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/string.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/timeutils.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/toml.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/trans.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/tree_sitter.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/yaml.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_code_analyser.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_files_helpers.py
RENAMED
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_get_interface.py
RENAMED
|
File without changes
|
|
File without changes
|
{ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_modules_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|