py-adtools 0.3.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.
@@ -0,0 +1,567 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-adtools
3
+ Version: 0.3.2
4
+ Summary: Useful tools for parsing and evaluating Python programs for LLM-based algorithm design.
5
+ Author-email: Rui Zhang <rzhang.cs@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/RayZhhh/py-adtools
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: psutil
16
+ Requires-Dist: openai
17
+ Requires-Dist: ray<2.49.0,>=2.48.0
18
+ Requires-Dist: requests
19
+ Dynamic: license-file
20
+
21
+ # Useful code parser, sandbox, and evaluator for LLM-aided algorithm design/code optimization
22
+
23
+ ------
24
+
25
+ The figure demonstrates how a Python program is parsed into [PyCodeBlock](./adtools/py_code.py#L18-L33), [PyFunction](./adtools/py_code.py#L38-L115), [PyClass](./adtools/py_code.py#L118-L192), and [PyProgram](./adtools/py_code.py#L195-L242) via `adtools`.
26
+
27
+ ![pycode](./assets/pycode.png)
28
+
29
+ ------
30
+
31
+ ## Installation
32
+
33
+ > [!TIP]
34
+ >
35
+ > It is recommended to use Python >= 3.10.
36
+
37
+ Run the following instructions to install adtools.
38
+
39
+ ```shell
40
+ pip install git+https://github.com/RayZhhh/py-adtools.git
41
+ ```
42
+
43
+ Or install via pip:
44
+
45
+ ```shell
46
+ pip install py-adtools
47
+ ```
48
+
49
+ ## Code Parsing with [py_code](./algolm/py-adtools/adtools/py_code.py#L0-L560)
50
+
51
+ [adtools.py_code](./adtools/py_code.py#L0-L560) provides robust parsing of Python programs into structured components
52
+ that can be easily manipulated, modified, and analyzed.
53
+
54
+ ### Core Components
55
+
56
+ The parser decomposes Python code into four main data structures:
57
+
58
+ | **Component** | **Description** | **Key Attributes** |
59
+ |-----------------|------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------|
60
+ | **PyProgram** | Represents the entire file. It maintains the exact sequence of scripts, functions, and classes. | `functions`, `classes`, `scripts`, `elements` |
61
+ | **PyFunction** | Represents a top-level function or a class method. You can modify its signature, decorators, docstring, or body dynamically. | `name`, `args`, `body`, `docstring`, `decorator`, `return_type` |
62
+ | **PyClass** | Represents a class definition. It serves as a container for methods and class-level statements. | `name`, `bases`, `functions` (methods), `body` |
63
+ | **PyCodeBlock** | Represents raw code segments, such as imports, global variables, or specific logic blocks inside classes. | `code` |
64
+
65
+ ### Basic Usage
66
+
67
+ ```python
68
+ from adtools import PyProgram
69
+
70
+ code = r"""
71
+ import ast, numba # This part will be parsed into PyCodeBlock
72
+ import numpy as np
73
+
74
+ @numba.jit() # This part will be parsed into PyFunction
75
+ def function(arg1, arg2=True):
76
+ '''Docstring.
77
+ This is a function.
78
+ '''
79
+ if arg2:
80
+ return arg1 * 2
81
+ else:
82
+ return arg1 * 4
83
+
84
+ @some.decorators() # This part will be parsed into PyClass
85
+ class PythonClass(BaseClass):
86
+ '''Docstring.'''
87
+ # Comments
88
+ class_var1 = 1 # This part will be parsed into PyCodeBlock
89
+ class_var2 = 2 # and placed in PyClass.body
90
+
91
+ def __init__(self, x): # This part will be parsed into PyFunction
92
+ self.x = x # and placed in PyClass.functions
93
+
94
+ def method1(self):
95
+ '''Docstring.
96
+ This is a class method.
97
+ '''
98
+ return self.x * 10
99
+
100
+ @some.decorators()
101
+ def method2(self, x, y):
102
+ return x + y + self.method1(x)
103
+
104
+ @some.decorators(100)
105
+ class InnerClass: # This part will be parsed into PyCodeBlock
106
+ '''Docstring.'''
107
+ def __init__(self): # and placed in PyClass.body
108
+ ...
109
+
110
+ if __name__ == '__main__': # This part will be parsed into PyCodeBlock
111
+ res = function(1)
112
+ print(res)
113
+ res = PythonClass().method2(1, 2)
114
+ """
115
+
116
+ p = PyProgram.from_text(code, debug=True)
117
+ print(p)
118
+ print(f"-------------------------------------")
119
+ print(p.classes[0].functions[1])
120
+ print(f"-------------------------------------")
121
+ print(p.classes[0].functions[2].decorator)
122
+ print(f"-------------------------------------")
123
+ print(p.functions[0].name)
124
+
125
+ ```
126
+
127
+ ### Key Features
128
+
129
+ - **Preserves Code Structure**: Maintains original indentation and formatting
130
+ - **Handles Multiline Strings**: Properly preserves multiline string content without incorrect indentation
131
+ - **Access to Components**: Easily access functions, classes, and code blocks
132
+ - **Modify Code Elements**: Change function names, docstrings, or body content programmatically
133
+ - **Complete Program Representation**: [PyProgram](./adtools/py_code.py#L195-L242) maintains the exact sequence of
134
+ elements as they appear in the source code
135
+
136
+ ## Safe Execution with `sandbox`
137
+
138
+ `adtools.sandbox` provides a secure execution environment for running untrusted code. It isolates execution in a separate process, allowing for timeout management, resource protection, and output redirection.
139
+
140
+ ### Basic Usage
141
+
142
+ You can wrap any class or object with `SandboxExecutor` to execute its methods in a separate process.
143
+
144
+ ```python
145
+ import time
146
+ from typing import Any
147
+ from adtools.sandbox.sandbox_executor import SandboxExecutor
148
+
149
+ class SortAlgorithmEvaluator:
150
+ def evaluate_program(self, program: str) -> Any | None:
151
+ g = {}
152
+ exec(program, g)
153
+ sort_algo = g.get("merge_sort")
154
+ if not sort_algo: return None
155
+
156
+ input_data = [10, 2, 4, 76, 19, 29, 3, 5, 1]
157
+ start = time.time()
158
+ res = sort_algo(input_data)
159
+ duration = time.time() - start
160
+
161
+ return duration if res == sorted(input_data) else None
162
+
163
+ code_generated_by_llm = """
164
+ def merge_sort(arr):
165
+ if len(arr) <= 1: return arr
166
+ mid = len(arr) // 2
167
+ left = merge_sort(arr[:mid])
168
+ right = merge_sort(arr[mid:])
169
+
170
+ def merge(left, right):
171
+ result = []
172
+ i = j = 0
173
+ while i < len(left) and j < len(right):
174
+ if left[i] < right[j]:
175
+ result.append(left[i])
176
+ i += 1
177
+ else:
178
+ result.append(right[j])
179
+ j += 1
180
+ result.extend(left[i:])
181
+ result.extend(right[j:])
182
+ return result
183
+
184
+ return merge(left, right)
185
+ """
186
+
187
+ if __name__ == "__main__":
188
+ # Initialize SandboxExecutor with the worker instance
189
+ sandbox = SandboxExecutor(SortAlgorithmEvaluator(), debug_mode=True)
190
+
191
+ # Securely execute the method
192
+ score = sandbox.secure_execute(
193
+ "evaluate_program",
194
+ method_args=(code_generated_by_llm,),
195
+ timeout_seconds=10
196
+ )
197
+ print(f"Score: {score}")
198
+ ```
199
+
200
+ ### Sandbox Executors
201
+
202
+ `adtools` provides two sandbox implementations:
203
+
204
+ - **[SandboxExecutor](./adtools/sandbox/sandbox_executor.py)**
205
+ - Standard multiprocessing-based sandbox.
206
+ - Captures return values via shared memory.
207
+ - Supports timeout and output redirection.
208
+
209
+ - **[SandboxExecutorRay](./adtools/sandbox/sandbox_executor_ray.py)**
210
+ - Ray-based sandbox for distributed execution.
211
+ - Ideal for scenarios requiring stronger isolation or cluster-based evaluation.
212
+
213
+ ## Code Evaluation with `evaluator`
214
+
215
+ `adtools.evaluator` provides multiple secure evaluation options for running and testing Python code.
216
+
217
+ ### Basic Usage
218
+
219
+ ```python
220
+ import time
221
+ from typing import Dict, Callable, List, Any
222
+
223
+ from adtools.evaluator import PyEvaluator
224
+
225
+
226
+ class SortAlgorithmEvaluator(PyEvaluator):
227
+ def evaluate_program(
228
+ self,
229
+ program_str: str,
230
+ callable_functions_dict: Dict[str, Callable] | None,
231
+ callable_functions_list: List[Callable] | None,
232
+ callable_classes_dict: Dict[str, Callable] | None,
233
+ callable_classes_list: List[Callable] | None,
234
+ **kwargs,
235
+ ) -> Any | None:
236
+ """Evaluate a given sort algorithm program.
237
+ Args:
238
+ program_str : The raw program text.
239
+ callable_functions_dict: A dict maps function name to callable function.
240
+ callable_functions_list: A list of callable functions.
241
+ callable_classes_dict : A dict maps class name to callable class.
242
+ callable_classes_list : A list of callable classes.
243
+ Return:
244
+ Returns the evaluation result.
245
+ """
246
+ # Get the sort algorithm
247
+ sort_algo: Callable = callable_functions_dict["merge_sort"]
248
+ # Test data
249
+ input = [10, 2, 4, 76, 19, 29, 3, 5, 1]
250
+ # Compute execution time
251
+ start = time.time()
252
+ res = sort_algo(input)
253
+ duration = time.time() - start
254
+ if res == sorted(input): # If the result is correct
255
+ return duration # Return the execution time as the score of the algorithm
256
+ else:
257
+ return None # Return None as the algorithm is incorrect
258
+
259
+
260
+ code_generated_by_llm = """
261
+ def merge_sort(arr):
262
+ if len(arr) <= 1:
263
+ return arr
264
+
265
+ mid = len(arr) // 2
266
+ left = merge_sort(arr[:mid])
267
+ right = merge_sort(arr[mid:])
268
+
269
+ return merge(left, right)
270
+
271
+ def merge(left, right):
272
+ result = []
273
+ i = j = 0
274
+
275
+ while i < len(left) and j < len(right):
276
+ if left[i] < right[j]:
277
+ result.append(left[i])
278
+ i += 1
279
+ else:
280
+ result.append(right[j])
281
+ j += 1
282
+
283
+ result.extend(left[i:])
284
+ result.extend(right[j:])
285
+
286
+ return result
287
+ """
288
+
289
+ harmful_code_generated_by_llm = """
290
+ def merge_sort(arr):
291
+ print('I am harmful') # There will be no output since we redirect STDOUT to /dev/null by default.
292
+ while True:
293
+ pass
294
+ """
295
+
296
+ if __name__ == "__main__":
297
+ evaluator = SortAlgorithmEvaluator()
298
+
299
+ # Evaluate
300
+ score = evaluator._exec_and_get_res(code_generated_by_llm)
301
+ print(f"Score: {score}")
302
+
303
+ # Secure evaluate (the evaluation is executed in a sandbox process)
304
+ score = evaluator.secure_evaluate(code_generated_by_llm, timeout_seconds=10)
305
+ print(f"Score: {score}")
306
+
307
+ # Evaluate a harmful code, the evaluation will be terminated within 10 seconds
308
+ # We will obtain a score of `None` due to the violation of time restriction
309
+ score = evaluator.secure_evaluate(harmful_code_generated_by_llm, timeout_seconds=10)
310
+ print(f"Score: {score}")
311
+
312
+ ```
313
+
314
+ ### Evaluator Types and Their Characteristics
315
+
316
+ `adtools` provides two different evaluator implementations, each optimized for different scenarios:
317
+
318
+ - **[PyEvaluator](./adtools/evaluator/py_evaluator.py#L36-L309)**
319
+ - *Uses shared memory* for extremely large return objects (e.g., large tensors)
320
+ - *Avoids pickle serialization overhead* for massive data
321
+ - *Best for high-performance scenarios* with very large result objects
322
+ - *Use case*: Evaluating ML algorithms that produce large tensors or arrays
323
+
324
+ - **[PyEvaluatorRay](./adtools/evaluator/py_evaluator_ray.py#L23-L209)**
325
+ - *Leverages Ray* for distributed, secure evaluation
326
+ - *Supports zero-copy return* of large objects
327
+ - *Ideal for cluster environments* and when maximum isolation is required
328
+ - *Use case*: Large-scale evaluation across multiple machines or when using GPU resources
329
+
330
+ All evaluators share the same interface through the abstract [PyEvaluator](./adtools/evaluator/py_evaluator.py#L36-L309)
331
+ class, making it easy to switch between implementations based on your specific needs.
332
+
333
+ ## Practical Applications
334
+
335
+ ### Parser for Code Manipulation
336
+
337
+ The parser is designed to handle complex scenarios, including **multiline strings**, **decorators**, and **indentation
338
+ management**.
339
+
340
+ ```python
341
+ from adtools import PyProgram
342
+
343
+ # A complex piece of code with imports, decorators, and a class
344
+ code = r'''
345
+ import numpy as np
346
+
347
+ @jit(nopython=True)
348
+ def heuristics(x):
349
+ """Calculates the heuristic value."""
350
+ return x * 0.5
351
+
352
+ class EvolutionStrategy:
353
+ population_size = 100
354
+
355
+ def __init__(self, mu, lambda_):
356
+ self.mu = mu
357
+ self.lambda_ = lambda_
358
+
359
+ def mutate(self, individual):
360
+ # Apply mutation
361
+ return individual + np.random.normal(0, 1)
362
+ '''
363
+
364
+ # 1. Parse the program
365
+ program = PyProgram.from_text(code)
366
+
367
+ # 2. Access and Modify Functions
368
+ func = program.functions[0]
369
+ print(f"Function detected: {func.name}")
370
+ # Output: Function detected: heuristics
371
+
372
+ # Modify the function programmatically
373
+ func.name = "fast_heuristics"
374
+ func.decorator = None # Remove decorator
375
+ func.docstring = "Optimized heuristic calculation."
376
+
377
+ # 3. Access Class Methods
378
+ cls_obj = program.classes[0]
379
+ init_method = cls_obj.functions[0]
380
+ mutate_method = cls_obj.functions[1]
381
+
382
+ print(f"Class: {cls_obj.name}, Method: {mutate_method.name}")
383
+ # Output: Class: EvolutionStrategy, Method: mutate
384
+
385
+ # 4. Generate the modified code
386
+ # The PyProgram object reconstructs the code preserving the original order
387
+ print("\n--- Reconstructed Code ---")
388
+ print(program)
389
+
390
+ ```
391
+
392
+ ### Parser for Prompt Construction
393
+
394
+ `adtools` is particularly powerful for LLM-based algorithm design, where you need to manage populations of generated
395
+ code, standardize formats for prompts, or inject generated logic into existing templates.
396
+
397
+ In LLM-based Automated Algorithm Design (LLM-AAD), you often maintain a population of algorithms. You may need to rename
398
+ them (e.g., `v1`, `v2`), standardize their docstrings for the context, or remove docstrings to save token costs before
399
+ feeding them back into the LLM.
400
+
401
+ ```python
402
+ from adtools import PyFunction
403
+
404
+ # Assume LLM generated two variants of a crossover algorithm
405
+ llm_output_1 = '''
406
+ def crossover(p1, p2):
407
+ """Single point crossover."""
408
+ point = len(p1) // 2
409
+ return p1[:point] + p2[point:], p2[:point] + p1[point:]
410
+ '''
411
+
412
+ llm_output_2 = """
413
+ def crossover_op(parent_a, parent_b):
414
+ # This is a uniform crossover
415
+ mask = [True, False] * (len(parent_a) // 2)
416
+ return [a if m else b for a, b, m in zip(parent_a, parent_b, mask)]
417
+ """
418
+
419
+ # Parse the functions
420
+ func_v1 = PyFunction.extract_first_function_from_text(llm_output_1)
421
+ func_v2 = PyFunction.extract_first_function_from_text(llm_output_2)
422
+
423
+ # --- Modification Logic ---
424
+
425
+ # 1. Standardize Naming: Rename to v1 and v2
426
+ func_v1.name = "crossover_v1"
427
+ func_v2.name = "crossover_v2"
428
+
429
+ # 2. Docstring Management:
430
+ # For v1: Enforce a specific docstring format for the prompt
431
+ func_v1.docstring = "Variant 1: Implementation of Single Point Crossover."
432
+
433
+ # For v2: Remove docstring entirely (e.g., to reduce context window usage)
434
+ func_v2.docstring = None
435
+
436
+ # --- Construct Prompt ---
437
+
438
+ prompt = "Here are the two crossover algorithms currently in the population:\n\n"
439
+ prompt += str(func_v1) + "\n"
440
+ prompt += str(func_v2) + "\n"
441
+ prompt += "Please generate a v3 that combines the best features of both."
442
+
443
+ print(prompt)
444
+
445
+ ```
446
+
447
+ **Output:**
448
+
449
+ ```text
450
+ Here are the two crossover algorithms currently in the population:
451
+
452
+ def crossover_v1(p1, p2):
453
+ """Variant 1: Implementation of Single Point Crossover."""
454
+ point = len(p1) // 2
455
+ return p1[:point] + p2[point:], p2[:point] + p1[point:]
456
+
457
+ def crossover_v2(parent_a, parent_b):
458
+ # This is a uniform crossover
459
+ mask = [True, False] * (len(parent_a) // 2)
460
+ return [a if m else b for a, b, m in zip(parent_a, parent_b, mask)]
461
+
462
+ Please generate a v3 that combines the best features of both.
463
+ ```
464
+
465
+ ### Secure Code Evaluation using Evaluators
466
+
467
+ When evaluating code generated by LLMs, safety and reliability are critical:
468
+
469
+ ```python
470
+ import time
471
+ from adtools.evaluator import PyEvaluator
472
+ from typing import Dict, Callable, List
473
+
474
+
475
+ class AlgorithmValidator(PyEvaluator):
476
+ def evaluate_program(
477
+ self,
478
+ program_str: str,
479
+ callable_functions_dict: Dict[str, Callable] | None,
480
+ callable_functions_list: List[Callable] | None,
481
+ callable_classes_dict: Dict[str, Callable] | None,
482
+ callable_classes_list: List[Callable] | None,
483
+ **kwargs
484
+ ) -> dict:
485
+ results = {"correct": 0, "total": 0, "time": 0}
486
+
487
+ try:
488
+ # Get the sorting function
489
+ sort_func = callable_functions_dict.get("sort_algorithm")
490
+ if not sort_func:
491
+ return {**results, "error": "Missing required function"}
492
+
493
+ # Test with multiple inputs
494
+ test_cases = [
495
+ [5, 3, 1, 4, 2],
496
+ [1, 2, 3, 4, 5],
497
+ [5, 4, 3, 2, 1],
498
+ list(range(100)), # Large test case
499
+ [],
500
+ ]
501
+
502
+ for case in test_cases:
503
+ start = time.time()
504
+ result = sort_func(
505
+ case[:]
506
+ ) # Pass a copy to avoid in-place modification
507
+ duration = time.time() - start
508
+
509
+ results["total"] += 1
510
+ if result == sorted(case):
511
+ results["correct"] += 1
512
+ results["time"] += duration
513
+
514
+ except Exception as e:
515
+ results["error"] = str(e)
516
+
517
+ return results
518
+
519
+
520
+ # Example usage with potentially problematic code
521
+ problematic_code = """
522
+ def sort_algorithm(arr):
523
+ # This implementation has a bug for empty arrays
524
+ if not arr:
525
+ return [] # Missing this case would cause failure
526
+
527
+ # Implementation with potential infinite loop
528
+ i = 0
529
+ while i < len(arr) - 1:
530
+ if arr[i] > arr[i+1]:
531
+ arr[i], arr[i+1] = arr[i+1], arr[i]
532
+ i = 0 # Reset to beginning after swap
533
+ else:
534
+ i += 1
535
+ return arr
536
+ """
537
+
538
+ malicious_code = """
539
+ def sort_algorithm(arr):
540
+ import time
541
+ time.sleep(15) # Exceeds timeout
542
+ return sorted(arr)
543
+ """
544
+
545
+ validator = AlgorithmValidator()
546
+ print(validator.secure_evaluate(problematic_code, timeout_seconds=5))
547
+ print(validator.secure_evaluate(malicious_code, timeout_seconds=5))
548
+
549
+ ```
550
+
551
+ This demonstrates how `adtools` handles:
552
+
553
+ - **Timeout protection**: Malicious code with infinite loops is terminated
554
+ - **Error isolation**: Exceptions in evaluated code don't crash your main process
555
+ - **Output redirection**: Prevents unwanted print statements from cluttering your console
556
+ - **Resource management**: Proper cleanup of processes and shared resources
557
+
558
+ The evaluation framework ensures that even if the code contains errors, infinite loops, or attempts to access system
559
+ resources, your main application remains safe and responsive.
560
+
561
+ ## License
562
+
563
+ This project is licensed under the **MIT License**. See the [LICENSE](./LICENSE) file for details.
564
+
565
+ ## Contact & Feedback
566
+
567
+ If you have any questions, encounter bugs, or have suggestions for improvement, please feel free to [open an issue](https://github.com/RayZhhh/py-adtools/issues) or contact us. Your contributions and feedback are highly appreciated!
@@ -0,0 +1,22 @@
1
+ adtools/__init__.py,sha256=vf6o3ORG-PrgJ0d7yOjOn9QXZvJWDz8nOEyC6r7tU2U,72
2
+ adtools/cli.py,sha256=DiKZ2f1sfrAHQuB7Mh01AgUqyeHmh3Nnka6umTcwlNE,2315
3
+ adtools/py_code.py,sha256=gIJ6mAIlF_aSCUv2VE4rOmZeS_40hzbI5mgWzEgpeJk,22582
4
+ adtools/evaluator/__init__.py,sha256=ZkgKlBPSeUeWPlZxtJgNQYlDc0RspnAuRDHiH_pLR-Y,117
5
+ adtools/evaluator/auto_server.py,sha256=DscbWaVMxzeuZeV6qDyWkLQpDhn-LOSjd1BieZj9enw,8215
6
+ adtools/evaluator/py_evaluator.py,sha256=JqbvdJOd8APHebHSybMegMB_3xiAGrN5AonCKiSClRM,6709
7
+ adtools/evaluator/py_evaluator_ray.py,sha256=unfqXqpWYsIpBbuiKKHqOQVMl9gxfA542OP_8UrGsnQ,4065
8
+ adtools/lm/__init__.py,sha256=ppzCTJk9Ny-RLPyxarrCwu1kmr4homHkwiDsGHI-P4o,185
9
+ adtools/lm/lm_base.py,sha256=qUHyR5jYeIllnYoR4IbSIKJZLaigW7329osVuwD0btk,1818
10
+ adtools/lm/openai_api.py,sha256=l-qpC55L-scJUWf7DLnjimh9l2jfdGGDrXs5nAYS7NM,3841
11
+ adtools/lm/sglang_server.py,sha256=ClhMRV2pNFbQf_mX8kaUKMEfWgyHay6DqI6RWCMN8K0,15665
12
+ adtools/lm/vllm_server.py,sha256=4i76FLxIDemaUtJbULBaO5efTvh8nB7YJOyetzpQ36Q,16365
13
+ adtools/sandbox/__init__.py,sha256=SW9xejOgC2o5KFauuRcRoAEoI5Q9M-1W7QL3_uvNvbQ,147
14
+ adtools/sandbox/sandbox_executor.py,sha256=H4UeqQeAKcwSCJ8T4G7K70rAJZlmZifTMuHezfrLvck,10065
15
+ adtools/sandbox/sandbox_executor_ray.py,sha256=1w3RPGAqHk3fD1ZzQFRsgmd1I1dsVgMql36hIr0tTHw,6853
16
+ adtools/sandbox/utils.py,sha256=kxIy37T1XIvAtdbPKLZO5jM4PFpZxknlKxPpAREyqMc,1111
17
+ py_adtools-0.3.2.dist-info/licenses/LICENSE,sha256=E5GGyecx3y5h2gcEGQloF-rDY9wbaef5IHjRsvtFbt8,1065
18
+ py_adtools-0.3.2.dist-info/METADATA,sha256=0eGGxvIypsAeCocdO_fILVCPB2_nNY80aJ0_-ETGiaA,18940
19
+ py_adtools-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ py_adtools-0.3.2.dist-info/entry_points.txt,sha256=s-kKqZwk0k95NFGLNx7zd5Rdn0jmZkuu1VkN59zZUMw,45
21
+ py_adtools-0.3.2.dist-info/top_level.txt,sha256=X2kKzmJFDAKR2FWCij5pfMG9pVVjVUomyl4e-1VLXIk,8
22
+ py_adtools-0.3.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ adtools = adtools.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rui Zhang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ adtools