ghoshell-common 0.4.0__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.
@@ -0,0 +1,209 @@
1
+ from abc import abstractmethod
2
+ from typing import Optional, Iterable, Protocol, Dict
3
+ import os
4
+ from ghoshell_container import Container, Provider
5
+ import fnmatch
6
+
7
+ __all__ = ['Storage', 'FileStorage', 'DefaultFileStorage', 'FileStorageProvider', 'MemoryStorage']
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 MemoryStorage(Storage):
159
+
160
+ def __init__(self, dir_: str):
161
+ self._dir: str = dir_
162
+ self._children: Dict[str, Storage] = {}
163
+ self._data: Dict[str, bytes] = {}
164
+
165
+ def sub_storage(self, relative_path: str) -> "Storage":
166
+ dir_ = os.path.join(self._dir, relative_path)
167
+ if dir_ in self._children:
168
+ return self._children[dir_]
169
+ sub = MemoryStorage(dir_)
170
+ self._children[dir_] = sub
171
+ return sub
172
+
173
+ def get(self, file_path: str) -> bytes:
174
+ _path = os.path.join(self._dir, file_path)
175
+ if _path not in self._data:
176
+ raise FileNotFoundError(f"file {file_path} is not found")
177
+ return self._data[_path]
178
+
179
+ def remove(self, file_path: str) -> None:
180
+ _path = os.path.join(self._dir, file_path)
181
+ if _path in self._data:
182
+ del self._data[_path]
183
+
184
+ def exists(self, file_path: str) -> bool:
185
+ _path = os.path.join(self._dir, file_path)
186
+ return _path in self._data
187
+
188
+ def put(self, file_path: str, content: bytes) -> None:
189
+ _path = os.path.join(self._dir, file_path)
190
+ self._data[_path] = content
191
+
192
+ def dir(self, prefix_dir: str, recursive: bool, patten: Optional[str] = None) -> Iterable[str]:
193
+ for dir_ in self._children.keys():
194
+ yield dir_[len(self._dir):]
195
+
196
+
197
+ class FileStorageProvider(Provider[FileStorage]):
198
+
199
+ def __init__(self, dir_: str):
200
+ self._dir: str = dir_
201
+
202
+ def singleton(self) -> bool:
203
+ return True
204
+
205
+ def aliases(self) -> Iterable[Storage]:
206
+ yield Storage
207
+
208
+ def factory(self, con: Container) -> Optional[Storage]:
209
+ return DefaultFileStorage(self._dir)
@@ -0,0 +1,91 @@
1
+ import os
2
+ from abc import ABC, abstractmethod
3
+ from typing import Optional
4
+
5
+ from ghoshell_common.contracts.storage import Storage, DefaultFileStorage, MemoryStorage
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) -> Storage:
21
+ """
22
+ workspace 根 storage.
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ def configs(self) -> Storage:
28
+ """
29
+ 配置文件存储路径.
30
+ """
31
+ pass
32
+
33
+ @abstractmethod
34
+ def runtime(self) -> Storage:
35
+ """
36
+ 运行时数据存储路径.
37
+ """
38
+ pass
39
+
40
+ @abstractmethod
41
+ def assets(self) -> Storage:
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
+ if not self._ws_root_dir:
54
+ self._root = MemoryStorage(self._ws_root_dir)
55
+ else:
56
+ self._root = DefaultFileStorage(workspace_dir)
57
+
58
+ def root(self) -> Storage:
59
+ return self._root
60
+
61
+ def configs(self) -> Storage:
62
+ return self._root.sub_storage("configs")
63
+
64
+ def runtime(self) -> Storage:
65
+ return self._root.sub_storage("runtime")
66
+
67
+ def assets(self) -> Storage:
68
+ return self._root.sub_storage("assets")
69
+
70
+
71
+ class LocalWorkspaceProvider(Provider[Workspace]):
72
+
73
+ def __init__(
74
+ self,
75
+ workspace_dir: str = "",
76
+ stub_dir: Optional[str] = None,
77
+ ):
78
+ if workspace_dir == "":
79
+ # 使用脚本运行的路径作为 workspace.
80
+ workspace_dir = abspath(workspace_dir)
81
+ self._ws_dir = workspace_dir
82
+ self._stub_dir = abspath(stub_dir) if stub_dir else None
83
+
84
+ def singleton(self) -> bool:
85
+ return True
86
+
87
+ def factory(self, con: Container) -> Optional[Workspace]:
88
+ if self._ws_dir and self._stub_dir and not os.path.exists(self._stub_dir):
89
+ os.makedirs(self._stub_dir)
90
+ shutil.copytree(self._stub_dir, self._ws_dir)
91
+ return LocalWorkspace(self._ws_dir)
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from abc import ABC, abstractmethod
5
+ from typing import Union, Any, TypeVar, Type, Optional, Protocol
6
+ from typing_extensions import Required, Self, TypedDict
7
+ from types import ModuleType
8
+ from pydantic import BaseModel
9
+ from ghoshell_common.helpers import generate_import_path, import_from_path, parse_import_path_module_and_attr_name
10
+ import inspect
11
+ import pickle
12
+ import base64
13
+ import yaml
14
+
15
+ __all__ = [
16
+
17
+ 'to_entity_meta', 'from_entity_meta', 'get_entity',
18
+ 'is_entity_type',
19
+ 'EntityMeta',
20
+ 'Entity', 'EntityType',
21
+ 'EntityClass', 'ModelEntity',
22
+
23
+ 'ModelEntityMeta',
24
+ 'to_entity_model_meta',
25
+ 'from_entity_model_meta',
26
+
27
+ ]
28
+
29
+ """
30
+ Experimental Feature
31
+ """
32
+
33
+
34
+ class Entity(Protocol):
35
+ """
36
+ Experimental interface
37
+
38
+ Protocol to define a class that generate transportable data (EntityMeta)
39
+ Instead of pickle, I want Entity is a light-weighted language independent protocol,
40
+ suite for JSON RPC or JSON API e.t.c.
41
+ """
42
+
43
+ @abstractmethod
44
+ def __to_entity_meta__(self) -> EntityMeta:
45
+ """
46
+ self-defined method to generate EntityMeta
47
+ """
48
+ pass
49
+
50
+ @classmethod
51
+ @abstractmethod
52
+ def __from_entity_meta__(cls, meta: EntityMeta) -> Self:
53
+ """
54
+ self-defined method to factory instance from EntityMeta
55
+ """
56
+ pass
57
+
58
+
59
+ class EntityClass(ABC):
60
+ """
61
+ Abstract Class for Entity
62
+
63
+ Experimental interface
64
+ """
65
+
66
+ @abstractmethod
67
+ def __to_entity_meta__(self) -> EntityMeta:
68
+ pass
69
+
70
+ @classmethod
71
+ @abstractmethod
72
+ def __from_entity_meta__(cls, meta: EntityMeta) -> Self:
73
+ pass
74
+
75
+
76
+ class ModelEntity(BaseModel, EntityClass, ABC):
77
+ """
78
+ pydantic Model that implements Entity interface
79
+ """
80
+
81
+ def __to_entity_meta__(self) -> EntityMeta:
82
+ return EntityMeta(
83
+ type=generate_import_path(self.__class__),
84
+ content=self.model_dump_json(exclude_defaults=True),
85
+ )
86
+
87
+ @classmethod
88
+ def __from_entity_meta__(cls, meta: EntityMeta) -> Self:
89
+ data = json.loads(meta['content'])
90
+ return cls(**data)
91
+
92
+
93
+ class EntityMeta(TypedDict):
94
+ """
95
+ Experimental feature
96
+
97
+ I want python has a light-weight way to marshal and unmarshal any instance and make it readable if allowed.
98
+ I found so many package-level implements like various kinds of Serializable etc.
99
+
100
+ So, I develop EntityMeta as a wrapper for any kind.
101
+ The EntityType will grow bigger with more marshaller, but do not affect who (me) is using the EntityMeta.
102
+ One day I can replace it with any better way inside the functions (but in-compatible)
103
+ """
104
+ type: Required[str]
105
+ content: Required[str]
106
+
107
+
108
+ class ModelEntityMeta(TypedDict):
109
+ """
110
+ Experimental feature
111
+
112
+ Data is dict, for pydantic.BaseModel
113
+ """
114
+ type: Required[str]
115
+ data: Required[dict]
116
+
117
+
118
+ EntityType = Union[Entity, EntityMeta, BaseModel]
119
+
120
+
121
+ def is_entity_type(value: Any) -> bool:
122
+ return hasattr(value, '__to_entity_meta__')
123
+
124
+
125
+ def to_entity_model_meta(value: BaseModel) -> ModelEntityMeta:
126
+ type_ = generate_import_path(type(value))
127
+ data = value.model_dump(exclude_defaults=True)
128
+ return ModelEntityMeta(type=type_, data=data)
129
+
130
+
131
+ def from_entity_model_meta(value: ModelEntityMeta) -> BaseModel:
132
+ cls = import_from_path(value['type'])
133
+ return cls(**value['data'])
134
+
135
+
136
+ def to_entity_meta(value: Union[EntityType, Any]) -> EntityMeta:
137
+ if value is None:
138
+ return EntityMeta(
139
+ type="None",
140
+ content="",
141
+ )
142
+ elif value is True or value is False:
143
+ return EntityMeta(type="bool", content=str(value))
144
+ elif isinstance(value, int):
145
+ return EntityMeta(type="int", content=str(value))
146
+ elif isinstance(value, str):
147
+ return EntityMeta(type="str", content=str(value))
148
+ elif isinstance(value, float):
149
+ return EntityMeta(type="float", content=str(value))
150
+ elif isinstance(value, list):
151
+ content = yaml.safe_dump(value)
152
+ return EntityMeta(type="list", content=content)
153
+ elif isinstance(value, dict):
154
+ content = yaml.safe_dump(value)
155
+ return EntityMeta(type="dict", content=content)
156
+ elif hasattr(value, '__to_entity_meta__'):
157
+ return getattr(value, '__to_entity_meta__')()
158
+ elif isinstance(value, BaseModel):
159
+ return EntityMeta(
160
+ type=generate_import_path(value.__class__),
161
+ content=value.model_dump_json(exclude_defaults=True),
162
+ )
163
+ elif inspect.isfunction(value):
164
+ return EntityMeta(
165
+ type=generate_import_path(value),
166
+ content="",
167
+ )
168
+ elif isinstance(value, BaseModel):
169
+ type_ = generate_import_path(value.__class__)
170
+ content = value.model_dump_json(exclude_defaults=True)
171
+ return EntityMeta(type=type_, content=content)
172
+ else:
173
+ content_bytes = pickle.dumps(value)
174
+ content = base64.encodebytes(content_bytes)
175
+ return EntityMeta(
176
+ type="pickle",
177
+ content=content.decode(),
178
+ )
179
+
180
+
181
+ T = TypeVar("T")
182
+
183
+
184
+ def get_entity(meta: EntityMeta, expect: Type[T]) -> T:
185
+ if meta is None:
186
+ raise ValueError("EntityMeta cannot be None")
187
+ entity = from_entity_meta(meta)
188
+ if not isinstance(entity, expect):
189
+ raise TypeError(f"Expected entity type {expect} but got {type(entity)}")
190
+ return entity
191
+
192
+
193
+ def from_entity_meta(meta: EntityMeta, module: Optional[ModuleType] = None) -> Any:
194
+ if meta is None:
195
+ return None
196
+ unmarshal_type = meta['type']
197
+ if unmarshal_type == "None":
198
+ return None
199
+ elif unmarshal_type == "int":
200
+ return int(meta['content'])
201
+ elif unmarshal_type == "str":
202
+ return str(meta['content'])
203
+ elif unmarshal_type == "bool":
204
+ return meta['content'] == "True"
205
+ elif unmarshal_type == "float":
206
+ return float(meta['content'])
207
+ elif unmarshal_type == "list" or unmarshal_type == "dict":
208
+ return yaml.safe_load(meta['content'])
209
+ elif unmarshal_type == 'pickle':
210
+ content = meta['content']
211
+ content_bytes = base64.decodebytes(content.encode())
212
+ return pickle.loads(content_bytes)
213
+
214
+ # raise if import error
215
+ cls = None
216
+ if module:
217
+ module_name, local_name = parse_import_path_module_and_attr_name(unmarshal_type)
218
+ if module_name == module.__name__:
219
+ cls = module.__dict__[local_name]
220
+ if cls is None:
221
+ cls = import_from_path(unmarshal_type)
222
+
223
+ if inspect.isfunction(cls):
224
+ return cls
225
+ # method is prior
226
+ elif hasattr(cls, "__from_entity_meta__"):
227
+ return getattr(cls, "__from_entity_meta__")(meta)
228
+
229
+ elif issubclass(cls, BaseModel):
230
+ data = json.loads(meta["content"])
231
+ return cls(**data)
232
+
233
+ raise TypeError(f"unsupported entity meta type: {unmarshal_type}")
@@ -0,0 +1,55 @@
1
+ from typing import TYPE_CHECKING
2
+ from ghoshell_common.helpers.dictionary import (dict_without_none, dict_without_zero)
3
+ from ghoshell_common.helpers.string import camel_to_snake
4
+ from ghoshell_common.helpers.yaml import yaml_pretty_dump, yaml_multiline_string_pipe
5
+ from ghoshell_common.helpers.modules import (
6
+ import_from_path,
7
+ import_class_from_path,
8
+ import_instance_from_path,
9
+ parse_import_path_module_and_attr_name,
10
+ join_import_module_and_spec,
11
+ get_module_attr,
12
+ generate_module_and_attr_name,
13
+ generate_import_path,
14
+ get_module_fullname_from_path,
15
+ Importer,
16
+ is_method_belongs_to_class,
17
+ get_calling_modulename,
18
+ rewrite_module,
19
+ rewrite_module_by_path,
20
+ create_module,
21
+ create_and_bind_module,
22
+ )
23
+ from ghoshell_common.helpers.io import BufferPrint
24
+ from ghoshell_common.helpers.timeutils import Timeleft, timestamp_datetime, timestamp, timestamp_ms
25
+ from ghoshell_common.helpers.hashes import md5, sha1, sha256
26
+ from ghoshell_common.helpers.trans import gettext, ngettext
27
+
28
+ from ghoshell_common.helpers.coding import reflect_module_code, unwrap
29
+ from ghoshell_common.helpers.openai import get_openai_key
30
+ from ghoshell_common.helpers.tree_sitter import tree_sitter_parse, code_syntax_check
31
+ from ghoshell_common.helpers.code_analyser import (
32
+ get_code_interface, get_code_interface_str,
33
+ get_attr_source_from_code, get_attr_interface_from_code,
34
+ )
35
+ from ghoshell_common.helpers.files import generate_directory_tree, list_dir, is_pathname_ignored
36
+
37
+ if TYPE_CHECKING:
38
+ from typing import Callable
39
+
40
+
41
+ # --- private methods --- #
42
+ def __uuid() -> str:
43
+ from uuid import uuid4
44
+ # keep uuid in 32 chars
45
+ return str(uuid4())
46
+
47
+
48
+ # --- facade --- #
49
+
50
+ uuid: "Callable[[], str]" = __uuid
51
+ """ patch this method to change global uuid generator"""
52
+
53
+
54
+ def uuid_md5() -> str:
55
+ return md5(uuid())