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.
Files changed (41) hide show
  1. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/PKG-INFO +2 -1
  2. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/pyproject.toml +2 -1
  3. ghoshell_common-0.4.0.dev1/src/ghoshell_common/cli/__init__.py +0 -0
  4. ghoshell_common-0.4.0.dev1/src/ghoshell_common/cli/main.py +34 -0
  5. ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/__init__.py +41 -0
  6. ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/assets.py +162 -0
  7. ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/configs.py +200 -0
  8. ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/logger.py +239 -0
  9. ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/storage.py +170 -0
  10. ghoshell_common-0.4.0.dev1/src/ghoshell_common/contracts/workspace.py +88 -0
  11. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/LICENSE +0 -0
  12. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/README.md +0 -0
  13. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/__init__.py +0 -0
  14. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/entity.py +0 -0
  15. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/__init__.py +0 -0
  16. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/code_analyser.py +0 -0
  17. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/coding.py +0 -0
  18. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/dictionary.py +0 -0
  19. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/files.py +0 -0
  20. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/hashes.py +0 -0
  21. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/io.py +0 -0
  22. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/modules.py +0 -0
  23. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/openai.py +0 -0
  24. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/string.py +0 -0
  25. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/timeutils.py +0 -0
  26. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/toml.py +0 -0
  27. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/trans.py +0 -0
  28. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/tree_sitter.py +0 -0
  29. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/helpers/yaml.py +0 -0
  30. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/identifier.py +0 -0
  31. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/src/ghoshell_common/prompter.py +0 -0
  32. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_code_analyser.py +0 -0
  33. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_files_helpers.py +0 -0
  34. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_get_interface.py +0 -0
  35. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_helpers.py +0 -0
  36. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_modules_helper.py +0 -0
  37. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_timeleft.py +0 -0
  38. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_toml.py +0 -0
  39. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/helpers/test_tree_sitter.py +0 -0
  40. {ghoshell_common-0.4.0.dev0 → ghoshell_common-0.4.0.dev1}/tests/test_entity.py +0 -0
  41. {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.dev0
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-dev"
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",
@@ -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)