stock-utils 0.2.3__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,14 @@
1
+ """Top-level package for stock_utils.
2
+
3
+ This module exposes the package version metadata used by the CLI and tooling.
4
+ """
5
+
6
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
7
+
8
+ try:
9
+ __version__=_pkg_version("stock_utils")
10
+
11
+ except PackageNotFoundError:
12
+ __version__="0+unknown"
13
+
14
+ __all__=["__version__"]
@@ -0,0 +1,6 @@
1
+ """Command-line entry point for the stock_utils package."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__=='__main__':
6
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Algorithm implementations and utility functions for stock_utils."""
@@ -0,0 +1,32 @@
1
+
2
+ """Binary search implementation for sorted collections."""
3
+
4
+
5
+ def binary_search(target, space):
6
+ """Return whether a target value exists in a sorted sequence.
7
+
8
+ Args:
9
+ target: Value to look for in the sequence.
10
+ space: Sorted sequence of comparable values.
11
+
12
+ Returns:
13
+ True if the target is present; otherwise False.
14
+ """
15
+ left = 0
16
+ right = len(space) - 1
17
+
18
+ while left <= right:
19
+ mid = (left + right) // 2
20
+ if space[mid] == target:
21
+ return True
22
+ elif space[mid] < target:
23
+ left = mid + 1
24
+ elif space[mid] > target:
25
+ right = mid - 1
26
+ return False
27
+
28
+ if __name__ == '__main__':
29
+ space = list(input('Enter the SORTED list:'))
30
+ target = int(input('Enter a search target:'))
31
+ print(binary_search(target, space))
32
+
@@ -0,0 +1,97 @@
1
+ """Diffie-Hellman key exchange example implementation."""
2
+
3
+ import random
4
+ random.seed(42)
5
+
6
+
7
+ def generate_shared_key(h, exp, p):
8
+ """Return the shared secret computed via modular exponentiation.
9
+
10
+ Args:
11
+ h: Public value received from the peer.
12
+ exp: Secret exponent used to derive the shared value.
13
+ p: Prime modulus used in the exchange.
14
+
15
+ Returns:
16
+ The computed shared key value.
17
+ """
18
+ k = (h ** exp) % p
19
+ return k
20
+
21
+
22
+ class Initiator:
23
+ """Diffie-Hellman initiator that generates a public value and key."""
24
+
25
+ def __init__(self):
26
+ """Initialize the initiator with a random modulus, base, and secret."""
27
+ self.p = random.randint(1, 50) # prime
28
+ self.g = random.randint(1, 50) # base
29
+ self.secret_x = random.randint(1, 50)
30
+
31
+ def calculate_ha(self):
32
+ """Compute the initiator's public value."""
33
+ self.ha = (self.g ** self.secret_x) % self.p
34
+
35
+ def send_p_and_g(self):
36
+ """Return the prime modulus and generator for the peer.
37
+
38
+ Returns:
39
+ A tuple of ``(p, g)`` values.
40
+ """
41
+ return self.p, self.g
42
+
43
+ def make_shared_key(self, hb):
44
+ """Compute the shared secret using the receiver's public value.
45
+
46
+ Args:
47
+ hb: Public value sent by the receiver.
48
+ """
49
+ self.shared_key = generate_shared_key(hb, self.secret_x, self.p)
50
+
51
+
52
+ class Receiver:
53
+ """Diffie-Hellman receiver that derives a shared secret."""
54
+
55
+ def __init__(self):
56
+ """Initialize the receiver with a random secret exponent."""
57
+ self.secret_y = random.randint(1, 50)
58
+
59
+ def calculate_hb(self, **kwargs):
60
+ """Compute the receiver's public value from the shared parameters.
61
+
62
+ Args:
63
+ **kwargs: Keyword arguments containing ``g`` and ``p``.
64
+ """
65
+ self.g = kwargs['g']
66
+ self.p = kwargs['p']
67
+ self.hb = (self.g ** self.secret_y) % self.p
68
+
69
+ def make_shared_key(self, ha):
70
+ """Compute the shared secret using the initiator's public value.
71
+
72
+ Args:
73
+ ha: Public value sent by the initiator.
74
+ """
75
+ self.shared_key = generate_shared_key(ha, self.secret_y, self.p)
76
+
77
+
78
+ def main():
79
+ """Run a simple Diffie-Hellman exchange and report whether keys match.
80
+
81
+ Returns:
82
+ True if the initiator and receiver compute the same shared key.
83
+ """
84
+ i = Initiator()
85
+ r = Receiver()
86
+ i.calculate_ha()
87
+ r.calculate_hb(g=i.g, p=i.p)
88
+ i.make_shared_key(r.hb)
89
+ r.make_shared_key(i.ha)
90
+ print(f'Initiator Diffie Hellman: {i.ha}')
91
+ print(f'Receiver Diffie Hellman: {r.hb}')
92
+ print(f'Shared key match?')
93
+ return i.shared_key == r.shared_key
94
+
95
+
96
+ if __name__ == '__main__':
97
+ print(main())
@@ -0,0 +1,164 @@
1
+ """Dijkstra's shortest path algorithm implementation."""
2
+
3
+ import itertools
4
+ from heapq import heappush, heappop
5
+
6
+ # Priority queue implementation
7
+ class PriorityQueue:
8
+ """Priority queue used by Dijkstra's shortest-path algorithm.
9
+
10
+ Entries are stored as ``[priority, count, task]`` tuples so tasks can be
11
+ compared by priority while preserving insertion order for ties.
12
+ """
13
+
14
+ def __init__(self):
15
+ """Initialize the priority queue."""
16
+ self.pq = [] # List of entries arranged in a heap
17
+ self.entry_finder = {} # mapping of tassk to entries
18
+ # REMOVED = '<removed-task>' # placeholder for a removed task
19
+ self.counter = itertools.count() # unique sequence count
20
+
21
+ def __len__(self):
22
+ """Return the number of queued tasks."""
23
+ return len(self.pq)
24
+
25
+ def add_task(self, priority, task):
26
+ """Add a task to the queue or update its priority when present.
27
+
28
+ Args:
29
+ priority: Relative priority for the task.
30
+ task: Task identifier to add or update.
31
+
32
+ Returns:
33
+ The queue instance for chaining convenience.
34
+ """
35
+ if task in self.entry_finder:
36
+ self.update_priority(priority, task)
37
+ return self
38
+ count = next(self.counter)
39
+ entry = [priority, count, task]
40
+ self.entry_finder[task] = entry
41
+ heappush(self.pq, entry)
42
+
43
+ def update_priority(self, priority, task):
44
+ """Update the priority for an existing queued task.
45
+
46
+ Args:
47
+ priority: New priority value.
48
+ task: Task identifier to update.
49
+ """
50
+ entry = self.entry_finder[task]
51
+ count = next(self.counter)
52
+ entry[0], entry[1] = priority, count
53
+
54
+ def pop_task(self):
55
+ """Remove and return the next task with the lowest priority.
56
+
57
+ Returns:
58
+ A tuple of ``(priority, task)`` for the next task.
59
+
60
+ Raises:
61
+ KeyError: If the queue is empty.
62
+ """
63
+ while self.pq:
64
+ priority, count, task = heappop(self.pq)
65
+ del self.entry_finder[task]
66
+ return priority, task
67
+ raise KeyError('YOU FAIL! pop from an empty priority queue')
68
+
69
+
70
+ class Graph:
71
+ """Graph container backed by an adjacency list."""
72
+
73
+ def __init__(self, adjacency_list):
74
+ """Initialize the graph with an adjacency list.
75
+
76
+ Args:
77
+ adjacency_list: Mapping of vertices to their outgoing edges.
78
+ """
79
+ self.adjacency_list = adjacency_list
80
+
81
+
82
+ class Vertex:
83
+ """Graph vertex containing a value."""
84
+
85
+ def __init__(self, value):
86
+ """Initialize a vertex.
87
+
88
+ Args:
89
+ value: Data stored on the vertex.
90
+ """
91
+ self.value = value
92
+
93
+
94
+ class Edge:
95
+ """Weighted edge connecting a vertex to another vertex."""
96
+
97
+ def __init__(self, distance, vertex):
98
+ """Initialize an edge.
99
+
100
+ Args:
101
+ distance: Cost of traversing the edge.
102
+ vertex: Destination vertex reached by the edge.
103
+ """
104
+ self.distance = distance
105
+ self.vertex = vertex
106
+
107
+
108
+ def dijkstra(graph, start, end):
109
+ """Find and print the shortest path from ``start`` to ``end``.
110
+
111
+ Args:
112
+ graph: Graph whose adjacency list contains weighted ``Edge`` objects.
113
+ start: Starting vertex.
114
+ end: Destination vertex.
115
+
116
+ Returns:
117
+ None. The shortest distance and path are printed to stdout.
118
+ """
119
+ previous = {v: None for v in graph.adjacency_list.keys()}
120
+ visited = {v: False for v in graph.adjacency_list.keys()}
121
+ distances = {v: float('inf') for v in graph.adjacency_list.keys()}
122
+ distances[start] = 0
123
+ queue = PriorityQueue()
124
+ queue.add_task(0, start)
125
+ path = []
126
+ while queue:
127
+ removed_distance, removed = queue.pop_task()
128
+ visited[removed] = True
129
+ if removed is end:
130
+ while previous[removed]:
131
+ path.append(removed.value)
132
+ removed = previous[removed]
133
+ path.append(start.value)
134
+ print(f'shortest distance to {end.value}: ', distances[end])
135
+ print(f'path to {end.value}: ', path[::-1])
136
+ return
137
+
138
+ for edge in graph.adjacency_list[removed]:
139
+ if visited[edge.vertex]:
140
+ continue
141
+ new_distance = removed_distance + edge.distance
142
+ if new_distance < distances[edge.vertex]:
143
+ distances[edge.vertex] = new_distance
144
+ previous[edge.vertex] = removed
145
+ queue.add_task(new_distance, edge.vertex)
146
+ return
147
+
148
+ # test
149
+ vertices = [Vertex('A'), Vertex('B'), Vertex('C'), Vertex('D'), Vertex('E'), Vertex('F'), Vertex('G'), Vertex('H')]
150
+ A, B, C, D, E, F, G, H = vertices
151
+ adj_list = {
152
+ A: [Edge(1.8, B), Edge(1.5, C), Edge(1.4, D)],
153
+ B: [Edge(1.8, A), Edge(1.6, E)],
154
+ C: [Edge(1.5, A), Edge(1.8, E), Edge(2.1, F)],
155
+ D: [Edge(1.4, A), Edge(2.7, F), Edge(2.4, G)],
156
+ E: [Edge(1.6, B), Edge(1.8, C), Edge(1.4, F), Edge(1.6, H)],
157
+ F: [Edge(2.1, C), Edge(2.7, D), Edge(1.4, E), Edge(1.3, G), Edge(1.2, H)],
158
+ G: [Edge(2.4, D), Edge(1.3, F), Edge(1.5, H)],
159
+ H: [Edge(1.6, E), Edge(1.2, F), Edge(1.5, G)]
160
+ }
161
+
162
+ test_graph = Graph(adj_list)
163
+
164
+ dijkstra(test_graph, start=A, end=H)
@@ -0,0 +1,27 @@
1
+ """Palindrome checking utilities."""
2
+
3
+
4
+ def is_palindrome(s: str) -> bool:
5
+ """Return whether a string reads the same forwards and backwards.
6
+
7
+ Args:
8
+ s: String to evaluate.
9
+
10
+ Returns:
11
+ True if the string is a palindrome; otherwise False.
12
+ """
13
+ left = 0
14
+ right = len(s) - 1
15
+
16
+ while left < right:
17
+ if s[left] != s[right]:
18
+ return False
19
+ left += 1
20
+ right -= 1
21
+
22
+ return True
23
+
24
+
25
+ if __name__ == '__main__':
26
+ word = input('Enter a word: ')
27
+ print(is_palindrome(word))
@@ -0,0 +1,38 @@
1
+ """Lexicographical string comparison utilities."""
2
+
3
+
4
+ def compare_strings(s1: str, s2: str) -> int:
5
+ """Compare two strings lexicographically.
6
+
7
+ Args:
8
+ s1: First string to compare.
9
+ s2: Second string to compare.
10
+
11
+ Returns:
12
+ -1 if ``s1`` sorts before ``s2``, 0 if they are equal, and 1 if ``s1``
13
+ sorts after ``s2``.
14
+ """
15
+ # return -1 if s1 < s2, 0 if s1 == s2, 1 if s1 > s2
16
+ if s1 == s2:
17
+ return 0
18
+ i, j = 0, 0
19
+ while i < len(s1) and j < len(s2):
20
+ if s1[i] < s2[j]:
21
+ return -1
22
+ elif s1[i] > s2[i]:
23
+ return 1
24
+ i += 1
25
+ j += 1
26
+
27
+ if len(s1) < len(s2):
28
+ return -1
29
+ else:
30
+ return 1
31
+
32
+
33
+
34
+ if __name__ == '__main__':
35
+ s1 = input('Enter the first string to compare:')
36
+ s2 = input('Enter the second string to compare:')
37
+
38
+ print(compare_strings(s1,s2))
@@ -0,0 +1,37 @@
1
+ """Longest palindromic substring search implementation."""
2
+
3
+
4
+ def find_longest_palindrome(s):
5
+ """Return the longest palindromic substring within a string.
6
+
7
+ Args:
8
+ s: String to inspect.
9
+
10
+ Returns:
11
+ The longest palindromic substring found in ``s``. If none exists, an
12
+ empty string is returned.
13
+ """
14
+ longest = ''
15
+ len_long = 0
16
+ for i in range(len(s)):
17
+ left, right = i, i
18
+ while left >= 0 and right < len(s) and s[left] == s[right]:
19
+ if (right - left + 1) > len_long:
20
+ longest = s[left:right + 1]
21
+ len_long = right - left + 1
22
+ left -= 1
23
+ right += 1
24
+
25
+ left, right = i, i + 1
26
+ while left >= 0 and right < len(s) and s[left] == s[right]:
27
+ if (right - left + 1) > len_long:
28
+ longest = s[left:right + 1]
29
+ len_long = right - left + 1
30
+ left -= 1
31
+ right += 1
32
+
33
+ return longest
34
+
35
+ if __name__ == '__main__':
36
+ word = input('Enter a word:')
37
+ print(find_longest_palindrome(word))
@@ -0,0 +1,29 @@
1
+ """In-place list reversal utility."""
2
+
3
+
4
+ def reverse_in_place(arr):
5
+ """Reverse a list in place.
6
+
7
+ Args:
8
+ arr: List to reverse.
9
+
10
+ Returns:
11
+ The reversed list.
12
+ """
13
+ left = 0
14
+ right = len(arr) - 1
15
+
16
+ while left < right:
17
+ temp = arr[left]
18
+ arr[left] = arr[right]
19
+ arr[right] = temp
20
+ left += 1
21
+ right -= 1
22
+
23
+ return arr
24
+
25
+
26
+ if __name__ == '__main__':
27
+ arr = input('Input values for the array as one input without spaces:')
28
+ arr_reversed = reverse_in_place(list(arr))
29
+ print(arr_reversed)
@@ -0,0 +1,34 @@
1
+ """Array rotation utilities."""
2
+
3
+
4
+ def rotate_array(nums: list[int], k: int) -> list[int]:
5
+ """Rotate a list to the right by ``k`` positions.
6
+
7
+ Args:
8
+ nums: Sequence of integers to rotate.
9
+ k: Number of positions to rotate. Values larger than the list length are
10
+ normalized modulo the list size.
11
+
12
+ Returns:
13
+ A new list with the elements rotated to the right by ``k`` positions.
14
+ """
15
+ if not nums or k == 0:
16
+ return nums[:]
17
+
18
+ # normalize k
19
+ n = len(nums)
20
+ k %= n
21
+ # set the index, at which to split
22
+ split = n - k
23
+ # rotating by slicing
24
+ ## combine the last k elements + the first k elements
25
+ return nums[split:] + nums[:split]
26
+
27
+
28
+
29
+ if __name__=='__main__':
30
+ nums = list(input('Enter items for the list as a string:'))
31
+ k = int(input('Enter a number of times to rotate (integer):'))
32
+ print(rotate_array(nums, k))
33
+
34
+
stock_utils/cli.py ADDED
@@ -0,0 +1,59 @@
1
+ """Command-line interface for stock_utils."""
2
+
3
+ import argparse
4
+ import importlib.metadata
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__
9
+ from .core import Orchestrator
10
+
11
+
12
+ def _build_parser() -> argparse.ArgumentParser:
13
+ """Build the command-line parser for the stock_utils CLI.
14
+
15
+ Returns:
16
+ Configured ``ArgumentParser`` instance with version and silent options.
17
+ """
18
+ prog = importlib.metadata.metadata("stock_utils")["Name"]
19
+ parser = argparse.ArgumentParser(prog=prog, description=(__doc__ or ''))
20
+ parser.add_argument(
21
+ '--version',
22
+ action='version',
23
+ version=f'{prog} {__version__}',
24
+ help='Show version and exit',
25
+ )
26
+
27
+ parser.add_argument(
28
+ '--silent',
29
+ action='store_true',
30
+ help='Disable audio.'
31
+ )
32
+
33
+ return parser
34
+
35
+
36
+ def main(argv: list[str]|None=None)->int:
37
+ """Run the stock_utils command-line entry point.
38
+
39
+ Args:
40
+ argv: Optional list of command-line arguments to parse. If omitted,
41
+ ``sys.argv[1:]`` is used.
42
+
43
+ Returns:
44
+ Exit status code for the CLI.
45
+ """
46
+ argv=sys.argv[1:] if argv is None else argv
47
+ parser = _build_parser()
48
+
49
+ args=parser.parse_args(argv)
50
+ if hasattr(args, "func"):
51
+ return int(args.func(args) or 0)
52
+ o = Orchestrator('Orchestrator')
53
+ o.menu_loop(args.silent)
54
+
55
+ return 0
56
+
57
+
58
+ if __name__=='__main__':
59
+ raise SystemExit(main())
stock_utils/core.py ADDED
@@ -0,0 +1,110 @@
1
+ """Core orchestration logic for launching project scripts."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from stock_utils.utils.console import console
6
+ from rich.prompt import Prompt, IntPrompt
7
+ from rich.table import Table
8
+ from rich.align import Align
9
+ from playsound3 import playsound
10
+
11
+
12
+ def _discover_scripts():
13
+ """Discover runnable Python scripts under the project modules.
14
+
15
+ Returns:
16
+ A dictionary keyed by module name containing sorted script names such as
17
+ ``"algo.binary_search"``.
18
+ """
19
+ src_dir = Path(__file__).resolve().parent
20
+ modules = ("algo", "math", "utils")
21
+ discovered = {}
22
+
23
+ for module in modules:
24
+ module_dir = src_dir / module
25
+ scripts = []
26
+
27
+ if module_dir.is_dir():
28
+ for file_path in sorted(module_dir.glob("*.py")):
29
+ if file_path.name == "__init__.py":
30
+ continue
31
+ scripts.append(f"{module}.{file_path.stem}")
32
+
33
+ discovered[module] = scripts
34
+
35
+ return discovered
36
+
37
+
38
+ def _chooser(options) -> int:
39
+ """Display a selection table and return the user's choice index.
40
+
41
+ Args:
42
+ options: Sequence of labels to show in the chooser.
43
+
44
+ Returns:
45
+ The selected option number, with ``0`` reserved for the exit choice.
46
+ """
47
+ table = Table(title="Options")
48
+ table.add_column('#', justify='center', style='cyan', no_wrap=True)
49
+ table.add_column('Title', justify='center', style='cyan')
50
+ choices = ['0']
51
+ for i, j in enumerate(options):
52
+ choice_string = str(i + 1)
53
+ table.add_row(choice_string, j)
54
+ choices.append(choice_string)
55
+ centered_table = Align.center(table, vertical='middle')
56
+ console.print(centered_table)
57
+ choice_message = Align.center('Enter your choice:', vertical='middle')
58
+ console.print(choice_message)
59
+ choice = IntPrompt.ask('', choices=choices, default=0)
60
+ return choice
61
+
62
+
63
+ class Orchestrator:
64
+ """Menu-driven orchestrator for launching project scripts."""
65
+
66
+ def __init__(self, name):
67
+ """Initialize the orchestrator with a display name.
68
+
69
+ Args:
70
+ name: Human-readable name for the orchestrator instance.
71
+ """
72
+ self.name = name
73
+ self.scripts = _discover_scripts()
74
+
75
+ def menu_loop(self, silent=False):
76
+ """Run the primary interactive menu until the user exits.
77
+
78
+ Args:
79
+ silent: If ``True``, suppress the startup audio.
80
+
81
+ Returns:
82
+ The exit status code chosen by the user.
83
+ """
84
+ sound = None
85
+ if not silent:
86
+ src_dir = Path(__file__).resolve().parent.parent
87
+ audio = src_dir / 'stock_utils/static/test_audio.mp3'
88
+ sound = playsound(audio, block=False)
89
+ status = 1
90
+ while status == 1:
91
+ mod_options = ('algo', 'math', 'utils')
92
+ mod_choice = _chooser(mod_options)
93
+ mod_choice -= 1
94
+ if mod_choice == -1 or mod_choice > len(mod_options) - 1:
95
+ status = mod_choice
96
+ else:
97
+ script_options = self.scripts[mod_options[mod_choice]]
98
+ script_choice = _chooser(script_options)
99
+ script_choice -= 1
100
+ if script_choice == -1 or script_choice > len(script_options):
101
+ status = script_choice
102
+ else:
103
+ s = self.scripts[mod_options[mod_choice]][script_choice]
104
+ module, stem = s.split(".")
105
+ script_path = Path(__file__).resolve().parent / module / f"{stem}.py"
106
+ subprocess.run(["python", str(script_path)])
107
+
108
+ if sound:
109
+ sound.stop()
110
+ return status
@@ -0,0 +1 @@
1
+ """Mathematical utilities and sample exercises for stock_utils."""