contaix 0.0.2__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.
- contaix/__init__.py +49 -0
- contaix/aggregation.py +86 -0
- contaix/code.py +252 -0
- contaix/markdown.py +890 -0
- contaix/urls.py +348 -0
- contaix/util.py +122 -0
- contaix-0.0.2.dist-info/METADATA +207 -0
- contaix-0.0.2.dist-info/RECORD +11 -0
- contaix-0.0.2.dist-info/WHEEL +5 -0
- contaix-0.0.2.dist-info/licenses/LICENSE +21 -0
- contaix-0.0.2.dist-info/top_level.txt +1 -0
contaix/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for working with AI contexts
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Tools to make contexts (knowledge bases for AI agents)
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
|
|
10
|
+
Download all articles from a markdown string and save them as PDF files:
|
|
11
|
+
|
|
12
|
+
>>> download_articles(md_string) # doctest: +SKIP
|
|
13
|
+
|
|
14
|
+
Verify URLs in a markdown string by checking their status codes
|
|
15
|
+
(useful when trying to verify if AI hallucinated the urls)
|
|
16
|
+
x`
|
|
17
|
+
>>> verify_urls(md_string) # doctest: +SKIP
|
|
18
|
+
|
|
19
|
+
Make an md file with all the code in a directory:
|
|
20
|
+
|
|
21
|
+
>>> md_string = code_aggregate(package_or_folder_or_github_url) # doctest: +SKIP
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# --------------------------------------------------------------------------------------
|
|
26
|
+
# General utilities
|
|
27
|
+
|
|
28
|
+
from contaix.util import (
|
|
29
|
+
identity,
|
|
30
|
+
fullpath,
|
|
31
|
+
url_to_contents,
|
|
32
|
+
save_to_file_and_return_file,
|
|
33
|
+
)
|
|
34
|
+
from contaix.aggregation import aggregate_store
|
|
35
|
+
from contaix.code import code_aggregate, PackageCodeContexts
|
|
36
|
+
from contaix.urls import (
|
|
37
|
+
extract_urls,
|
|
38
|
+
verify_urls,
|
|
39
|
+
download_articles,
|
|
40
|
+
download_articles_by_section,
|
|
41
|
+
)
|
|
42
|
+
from contaix.markdown import (
|
|
43
|
+
bytes_to_markdown,
|
|
44
|
+
bytes_store_to_markdown_store,
|
|
45
|
+
dflt_converters,
|
|
46
|
+
add_dflt_converter,
|
|
47
|
+
truncate_text,
|
|
48
|
+
notebook_to_markdown,
|
|
49
|
+
)
|
contaix/aggregation.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tools for aggregating contexts"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Mapping, Iterable, Union, Callable
|
|
4
|
+
from dol import TextFiles, filt_iter, wrap_kvs, Pipe, store_aggregate
|
|
5
|
+
from contaix.util import identity
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def aggregate_store(
|
|
9
|
+
store: Mapping,
|
|
10
|
+
*,
|
|
11
|
+
min_number_of_duplicated_lines: Optional[int] = None,
|
|
12
|
+
max_num_characters: Optional[int] = None,
|
|
13
|
+
exclude: Optional[Iterable] = None,
|
|
14
|
+
chk_size=None,
|
|
15
|
+
egress: Optional[Union[str, Callable]] = None,
|
|
16
|
+
**store_aggregate_kwargs,
|
|
17
|
+
):
|
|
18
|
+
"""
|
|
19
|
+
Aggregates and processes text files from a store.
|
|
20
|
+
|
|
21
|
+
Parameters:
|
|
22
|
+
store: The source store (e.g., TextFiles instance)
|
|
23
|
+
min_number_of_duplicated_lines (int): If not None, minimum block size for deduplication.
|
|
24
|
+
max_num_characters (int): Maximum number of characters per file.
|
|
25
|
+
exclude (set): Set of filenames to exclude.
|
|
26
|
+
chk_size (int): Chunk size for aggregation. If None, won't chunk
|
|
27
|
+
egress (str or callable): Template for output filenames or function to call on each chunk
|
|
28
|
+
**store_aggregate_kwargs: Additional keyword arguments to pass to store_aggregate.
|
|
29
|
+
"""
|
|
30
|
+
if exclude is None:
|
|
31
|
+
exclude = set()
|
|
32
|
+
|
|
33
|
+
wrappers = []
|
|
34
|
+
|
|
35
|
+
if exclude is not None:
|
|
36
|
+
wrappers.append(filt_iter(filt=lambda x: x not in exclude))
|
|
37
|
+
|
|
38
|
+
if min_number_of_duplicated_lines is not None:
|
|
39
|
+
from hg import deduplicate_string_lines
|
|
40
|
+
|
|
41
|
+
remove_duplicate_lines = wrap_kvs(
|
|
42
|
+
value_decoder=lambda v: deduplicate_string_lines(
|
|
43
|
+
v,
|
|
44
|
+
min_block_size=min_number_of_duplicated_lines,
|
|
45
|
+
return_removed_blocks=False,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
remove_duplicate_lines = identity
|
|
50
|
+
|
|
51
|
+
if max_num_characters is not None:
|
|
52
|
+
wrappers.append(wrap_kvs(value_decoder=lambda v: v[:max_num_characters]))
|
|
53
|
+
|
|
54
|
+
capped_num_characters = wrap_kvs(value_decoder=lambda v: v[:max_num_characters])
|
|
55
|
+
|
|
56
|
+
if len(wrappers) > 0:
|
|
57
|
+
store_wrap = Pipe(
|
|
58
|
+
filt_iter(filt=lambda x: x.endswith(".md") and x not in exclude),
|
|
59
|
+
remove_duplicate_lines,
|
|
60
|
+
capped_num_characters,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
store_wrap = identity
|
|
64
|
+
|
|
65
|
+
wrapped_store = store_wrap(store)
|
|
66
|
+
|
|
67
|
+
if isinstance(egress, str):
|
|
68
|
+
output_template = egress
|
|
69
|
+
egress = output_template.format
|
|
70
|
+
else:
|
|
71
|
+
assert egress is None or callable(
|
|
72
|
+
egress
|
|
73
|
+
), "egress must be None, str, or callable"
|
|
74
|
+
|
|
75
|
+
if isinstance(chk_size, int):
|
|
76
|
+
from lkj.chunking import chunk_iterable
|
|
77
|
+
|
|
78
|
+
chunks = chunk_iterable(wrapped_store, chk_size)
|
|
79
|
+
if egress is not None:
|
|
80
|
+
egress = "store_aggregate_{:02.0f}.md"
|
|
81
|
+
for i, sub_store in enumerate(chunks, 1):
|
|
82
|
+
store_aggregate(sub_store, egress=egress(i), **store_aggregate_kwargs)
|
|
83
|
+
else:
|
|
84
|
+
if egress is None:
|
|
85
|
+
egress = identity
|
|
86
|
+
return store_aggregate(wrapped_store, egress=egress, **store_aggregate_kwargs)
|
contaix/code.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Tools to make AI contexts from code bases"""
|
|
2
|
+
|
|
3
|
+
from typing import Mapping
|
|
4
|
+
import os
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import Any, Union, Callable, Optional, Iterable
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
from dol import store_aggregate, TextFiles, filt_iter, cached_keys
|
|
10
|
+
from contaix.util import fullpath, identity
|
|
11
|
+
|
|
12
|
+
DirectoryPathString = str
|
|
13
|
+
GithubUrl = str
|
|
14
|
+
RegexString = str
|
|
15
|
+
CodeSource = Union[DirectoryPathString, GithubUrl, Mapping]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_local_pkg_name(name: str) -> bool:
|
|
19
|
+
"""Returns True if and only if name is the name of a local package."""
|
|
20
|
+
try:
|
|
21
|
+
importlib.import_module(name)
|
|
22
|
+
return True
|
|
23
|
+
except ImportError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_code_source_dir_path(code_src: CodeSource) -> DirectoryPathString:
|
|
28
|
+
"""
|
|
29
|
+
Resolves code_src to a directory path string.
|
|
30
|
+
|
|
31
|
+
I
|
|
32
|
+
Args:
|
|
33
|
+
code_src (Any): The source of the code. Can be
|
|
34
|
+
a directory path string,
|
|
35
|
+
a GitHub URL,
|
|
36
|
+
or an imported package (must contain a __path__ atribute)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The resolved directory path.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
AssertionError: If the resolved path is not a valid directory.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(code_src, str):
|
|
45
|
+
# If it's a string, check if it's a directory
|
|
46
|
+
if os.path.isdir(code_src):
|
|
47
|
+
return os.path.abspath(code_src) # Return absolute path if it's a directory
|
|
48
|
+
elif "\n" not in code_src and "github" in code_src:
|
|
49
|
+
from hubcap import ensure_repo_folder # pip install hubcap
|
|
50
|
+
|
|
51
|
+
# If it's a GitHub URL, download the repository
|
|
52
|
+
repo_url = code_src
|
|
53
|
+
repo_path = ensure_repo_folder(repo_url)
|
|
54
|
+
return repo_path
|
|
55
|
+
elif is_local_pkg_name(code_src):
|
|
56
|
+
code_src = importlib.import_module(code_src) # import the package
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"Unsupported string format or non-directory: {code_src}")
|
|
59
|
+
# If it's not a string, check for __path__ attribute
|
|
60
|
+
if hasattr(code_src, "__path__"):
|
|
61
|
+
path_list = list(code_src.__path__)
|
|
62
|
+
assert len(path_list) == 1, (
|
|
63
|
+
f"The __path__ attribute should contain exactly one path, "
|
|
64
|
+
f"but found: {path_list}"
|
|
65
|
+
)
|
|
66
|
+
return os.path.abspath(path_list[0])
|
|
67
|
+
|
|
68
|
+
# If no valid resolution was found
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"Unable to resolve code_src to a valid directory path: {code_src}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def resolve_code_source(
|
|
75
|
+
code_src: CodeSource, keys_filt: Callable = lambda x: x.endswith(".py")
|
|
76
|
+
) -> Mapping:
|
|
77
|
+
"""
|
|
78
|
+
Will resolve code_src to a Mapping whose values are the code strings
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
code_src: The source of the code. Can be an explicit `Mapping`,
|
|
82
|
+
a directory path string,
|
|
83
|
+
a GitHub URL,
|
|
84
|
+
or an imported package (must contain a __path__ atribute)
|
|
85
|
+
keys_filt (Callable): A function to filter the keys. Defaults to lambda x: x.endswith('.py').
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(code_src, Mapping):
|
|
89
|
+
return code_src
|
|
90
|
+
else:
|
|
91
|
+
code_src_rootdir = resolve_code_source_dir_path(code_src)
|
|
92
|
+
return cached_keys(
|
|
93
|
+
filt_iter(TextFiles(code_src_rootdir), filt=keys_filt), keys_cache=sorted
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
Filepath = str
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def code_aggregate(
|
|
100
|
+
code_src: CodeSource,
|
|
101
|
+
*,
|
|
102
|
+
egress: Union[Callable, Filepath] = identity,
|
|
103
|
+
kv_to_item=lambda k, v: f"## {k}\n\n```python\n{v.strip()}\n```",
|
|
104
|
+
**store_aggregate_kwargs,
|
|
105
|
+
) -> Any:
|
|
106
|
+
"""
|
|
107
|
+
Aggregates all code segments from the given code source (folder, github url, store).
|
|
108
|
+
|
|
109
|
+
This is useful when you want to use AI to search and respond to questions about a
|
|
110
|
+
specific code base.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
code_src (dict): A dictionary where keys are references to the code (e.g., paths)
|
|
114
|
+
and values are code snippets or content.
|
|
115
|
+
egress (Union[Callable, str]): A function to apply to the aggregate before returning.
|
|
116
|
+
If a string, the aggregate will be saved to the file.
|
|
117
|
+
kv_to_item (Callable): A function that converts a key-value pairs to the
|
|
118
|
+
items that should be aggregated.
|
|
119
|
+
**store_aggregate_kwargs: Additional keyword arguments to pass to store_aggregate.
|
|
120
|
+
|
|
121
|
+
See dol.store_aggregate for more details.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Any: The aggregated code content, or the result of the egress function.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
|
|
128
|
+
>>> code_src = {
|
|
129
|
+
... 'module1.py': 'def foo(): pass',
|
|
130
|
+
... 'module2.py': 'def bar(): pass',
|
|
131
|
+
... 'module3.py': 'class Baz: pass',
|
|
132
|
+
... }
|
|
133
|
+
>>> print(code_aggregate(code_src))
|
|
134
|
+
## module1.py
|
|
135
|
+
<BLANKLINE>
|
|
136
|
+
```python
|
|
137
|
+
def foo(): pass
|
|
138
|
+
```
|
|
139
|
+
<BLANKLINE>
|
|
140
|
+
## module2.py
|
|
141
|
+
<BLANKLINE>
|
|
142
|
+
```python
|
|
143
|
+
def bar(): pass
|
|
144
|
+
```
|
|
145
|
+
<BLANKLINE>
|
|
146
|
+
## module3.py
|
|
147
|
+
<BLANKLINE>
|
|
148
|
+
```python
|
|
149
|
+
class Baz: pass
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Here, let's input an imported (third party) package, and have the function save the
|
|
153
|
+
result to a temporary file
|
|
154
|
+
|
|
155
|
+
>>> from tempfile import NamedTemporaryFile
|
|
156
|
+
>>> temp_file_name = NamedTemporaryFile().name
|
|
157
|
+
>>> import aix
|
|
158
|
+
>>> _ = code_aggregate(aix, egress=temp_file_name)
|
|
159
|
+
>>> print(open(temp_file_name).read(15))
|
|
160
|
+
## __init__.py
|
|
161
|
+
<BLANKLINE>
|
|
162
|
+
|
|
163
|
+
Tip: You can also import directly from the name (string) of the package by doing
|
|
164
|
+
`code_aggregate(__import__('aix'))` or more robustly,
|
|
165
|
+
`importlib.import_module('aix')`.
|
|
166
|
+
|
|
167
|
+
If you have hubcap installed, you can even get an aggregate of code from a GitHub
|
|
168
|
+
repository.
|
|
169
|
+
|
|
170
|
+
>>> string = code_aggregate('https://github.com/thorwhalen/aix') # doctest: +SKIP
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
code_store = resolve_code_source(code_src)
|
|
175
|
+
return store_aggregate(
|
|
176
|
+
code_store, egress=egress, kv_to_item=kv_to_item, **store_aggregate_kwargs
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PackageCodeContexts:
|
|
181
|
+
"""Manages aggregation and saves of the code of local packages"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, save_folder="."):
|
|
184
|
+
self.save_folder = fullpath(save_folder)
|
|
185
|
+
self.save_filepath = lambda *parts: os.path.join(self.save_folder, *parts)
|
|
186
|
+
|
|
187
|
+
def save_single(self, pkg: Union[str, ModuleType]):
|
|
188
|
+
"""
|
|
189
|
+
Aggregates and saves the code of a single local package.
|
|
190
|
+
|
|
191
|
+
Example:
|
|
192
|
+
|
|
193
|
+
To save a single package's code in a single file, in the current folder:
|
|
194
|
+
|
|
195
|
+
>>> PackageCodeContexts().save_single('aix') # doctest: +SKIP
|
|
196
|
+
|
|
197
|
+
or, to save multiple package's code in a single file, in a specific folder.
|
|
198
|
+
|
|
199
|
+
>>> PackageCodeContexts('some/folder/path').save_multiple_pkgs_code(['aix', 'dol']) # doctest: +SKIP
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
if isinstance(pkg, str):
|
|
203
|
+
pkg_name = pkg
|
|
204
|
+
pkg = importlib.import_module(pkg_name)
|
|
205
|
+
elif isinstance(pkg, ModuleType):
|
|
206
|
+
pkg_name = pkg.__name__
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError(f"Unsupported type for pkg: {pkg}")
|
|
209
|
+
|
|
210
|
+
filepath = self.save_filepath(f"{pkg_name}.py.md")
|
|
211
|
+
code_aggregate(pkg, egress=filepath)
|
|
212
|
+
|
|
213
|
+
def multiple_pkgs_code(
|
|
214
|
+
self,
|
|
215
|
+
name: Optional[Union[str, Iterable]] = None,
|
|
216
|
+
pkgs: list = (),
|
|
217
|
+
*,
|
|
218
|
+
pkg_secion_marker="#",
|
|
219
|
+
section_sepator="\n\n\n",
|
|
220
|
+
):
|
|
221
|
+
"""
|
|
222
|
+
Aggregates the code of multiple local packages in separate sections.
|
|
223
|
+
Saves the result to a file if a name is provided.
|
|
224
|
+
"""
|
|
225
|
+
if (
|
|
226
|
+
name is not None
|
|
227
|
+
and not isinstance(name, str)
|
|
228
|
+
and isinstance(name, Iterable)
|
|
229
|
+
and not pkgs
|
|
230
|
+
):
|
|
231
|
+
# if the first argument is an iterable, assume it's the list of packages
|
|
232
|
+
pkgs = name # the first argument is actually packages
|
|
233
|
+
name = None
|
|
234
|
+
|
|
235
|
+
def sections():
|
|
236
|
+
for pkg in pkgs:
|
|
237
|
+
yield f"{pkg_secion_marker} {pkg}\n\n" + code_aggregate(pkg)
|
|
238
|
+
|
|
239
|
+
md_string = section_sepator.join(sections())
|
|
240
|
+
|
|
241
|
+
if name is None:
|
|
242
|
+
return md_string
|
|
243
|
+
else:
|
|
244
|
+
assert isinstance(name, str), f"The first argument must a string: {name}"
|
|
245
|
+
# if not extension, add ".py.md" as the extension
|
|
246
|
+
if not os.path.splitext(name)[1]:
|
|
247
|
+
name = f"{name}.py.md"
|
|
248
|
+
|
|
249
|
+
with open(self.save_filepath(name), "w") as f:
|
|
250
|
+
f.write(md_string)
|
|
251
|
+
|
|
252
|
+
save_multiple_pkgs_code = multiple_pkgs_code # backwards compatibility alias
|