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 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