This commit is contained in:
Emile Clark-Boman 2025-06-24 16:42:17 +10:00
commit 09d4c52043
19 changed files with 542 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
__pycache__/

1
README Normal file
View file

@ -0,0 +1 @@
The "imbaud python library" (imp lib), or just imp for short!

0
imp/__init__.py Normal file
View file

19
imp/constants.py Normal file
View file

@ -0,0 +1,19 @@
'''
ASCII Character Ranges
'''
# Common
WHITESPACE = '\t\n\r\x0b\x0c'
DIGITS = '0123456789'
ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz'
ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALPHA = ALPHA_LOWER + ALPHA_UPPER
ALPHANUM = ALPHA + DIGITS
SYMBOLS = '!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
# Other
DIGITS_BIN = '01'
DIGITS_OCT = '01234567'
DIGITS_HEX_LOWER = '0123456789abcdef'
DIGITS_HEX_UPPER = '0123456789ABCDEF'
CHARSET_HEX = '0123456789abcdefABCDEF'
DIGITS_B64 = ALPHANUM + '+/'
CHARSET_B64 = DIGITS_B64 + '=' # Base64 charset contains = padding

0
imp/crypto/__init__.py Normal file
View file

32
imp/crypto/cyphers.py Normal file
View file

@ -0,0 +1,32 @@
from imp.constants import ALPHA_LOWER, ALPHA_UPPER
ERRMSG_ROT_CHARSET_DUPL = 'Bad charsets for ROT: found duplicate character \'{0}\''
ERRMSG_ROT_CHAR_UNKNOWN = 'No charset for ROT has character \'{0}\' (exception thrown since `ignore_noncharset=False`)'
'''
Substitution Cyphers
'''
def ROT(plaintext: str,
n: int,
charsets: list[str] = [ALPHA_LOWER, ALPHA_UPPER],
ignore_noncharset: bool = True) -> str:
cyphertext = ''
for c in plaintext:
index = None
for charset in charsets:
try:
i = charset.index(c)
# if we reached here then no ValueError occured
if index is not None:
raise ValueError(ERRMSG_ROT_CHARSET_DUPL.format(c))
break
except ValueError: pass
if index is not None:
cyphertext += charset[(i + n) % len(charset)]
elif not ignore_noncharset:
raise ValueError(ERRMSG_ROT_CHAR_UNKNOWN.format(c))
return cyphertext
# Common ROT aliases
def ROT13(plaintext: str) -> str: return ROT(plaintext, 13)
def caesar(plaintext: str) -> str: return ROT(plaintext, 1)

0
imp/math/__init__.py Normal file
View file

37
imp/math/groups.py Normal file
View file

@ -0,0 +1,37 @@
'''
This library exists to isolate all math functions
related to groups and their representations.
'''
from math import gcd
'''
Returns the multiplicative cyclic subgroup
generated by an element g modulo m.
Returns the cyclic subgroup as a list[int],
the order of that subgroup, and a boolean
indicating whether g is infinitely repeating
with period == ord<g> (or otherwise if it
terminates with g**ord<g> == 0).
'''
def cyclic_subgrp(g: int,
m: int,
ignore_zero: bool = True) -> tuple[list[int], int, bool]:
G = []
order = 0
periodic = True
a = 1 # start at identity
for _ in range(m):
a = (a * g) % m
if a == 0:
if not ignore_zero:
G.append(a)
order += 1
periodic = False
break
# check if we've reached something periodic
elif a in G[:1]:
break
G.append(a)
order += 1
return G, order, periodic

67
imp/math/primes.py Normal file
View file

@ -0,0 +1,67 @@
from math import gcd
'''
Euler's Totient (Phi) Function
'''
def totient(n: int) -> int:
phi = int(n > 1 and n)
for p in range(2, int(n ** .5) + 1):
if not n % p:
phi -= phi // p
while not n % p:
n //= p
#if n is > 1 it means it is prime
if n > 1: phi -= phi // n
return phi
'''
Tests the primality of an integer using its totient.
NOTE: If totient(n) has already been calculated
then pass it as the optional phi parameter.
'''
def is_prime(n: int, phi: int = None) -> bool:
return n - 1 == (phi if phi is not None else totient(n))
'''
Prime number generator function.
Returns the tuple (p, phi(p)) where p is prime
and phi is Euler's totient function.
'''
def prime_gen(yield_phi: bool = False) -> int | tuple[int, int]:
n = 1
while True:
n += 1
phi = totient(n)
if is_prime(n, phi=phi):
if yield_phi:
yield (n, phi)
else:
yield n
'''
Returns the prime factorisation of a number.
Returns a list of tuples (p, m) where p is
a prime factor and m is its multiplicity.
NOTE: uses a trial division algorithm
'''
def prime_factors(n: int) -> list[tuple[int, int]]:
phi = totient(n)
if is_prime(n, phi=phi):
return [(n, 1)]
factors = []
for p in prime_gen(yield_phi=False):
if p >= n:
break
# check if divisor
multiplicity = 0
while n % p == 0:
n //= p
multiplicity += 1
if multiplicity:
factors.append((p, multiplicity))
if is_prime(n):
break
if n != 1:
factors.append((n, 1))
return factors

2
imp/math/util.py Normal file
View file

@ -0,0 +1,2 @@
def digits(n: int) -> int:
return len(str(n))

1
imp/structs/__init__.py Normal file
View file

@ -0,0 +1 @@
from noether.lib.structs.__result import Result

19
imp/structs/__result.py Normal file
View file

@ -0,0 +1,19 @@
from enum import Enum
from typing import Optional
class Result:
def __init__(self,
success: bool,
reason: str,
value: Optional[any] = None) -> None:
self.success = success
self.reason = reason
self.value = value
@classmethod
def succeed(cls, value: any, reason: str = 'Ok') -> 'Result':
return cls(True, reason, value=value)
@classmethod
def fail(cls, reason: str, value: Optional[any] = None) -> 'Result':
return cls(False, reason, value=value)

0
sandbox/__init__.py Normal file
View file

0
sandbox/cli/__init__.py Normal file
View file

66
sandbox/cli/prompt.py Normal file
View file

@ -0,0 +1,66 @@
'''
===== Prompt Handling =====
This file implements the Prompt class, allowing for
inheritance and instantiation of Prompt objects.
The CLI class uses these instead of containing the
logic immediately within itself.
'''
from typing import Any
from noether.cli.style import *
from noether.lib.structs import Result
class Prompt:
DEFAULT_PROMPT = 'DEF> '
def __init__(self) -> None:
self._prompt = self.DEFAULT_PROMPT
'''
Prompt and await command via stdin.
'''
def prompt(self):
command = self.__request()
result = self._parse(command)
if result.success:
self._exec(result.value)
else:
self.__parse_error(result)
'''
!! OVERRIDE ON INHERITANCE !!
Handles the parsing of a given command.
'''
def _parse(self, command: str) -> Result:
return Result.succeed(command)
def __parse_error(self, error: Result) -> None:
err = f'{style(error.reason, Effect.ITALICS)}'
print(style(err, Effect.DIM))
'''
!! OVERRIDE ON INHERITANCE !!
Handles the execution of a command that
was successfully parsed by a Prompt inheritor.
'''
def _exec(self, command: Any) -> None:
pass
'''
Internal use only. Handles a raw request with no validation.
'''
def __request(self) -> None:
print(self.__get_prompt(), end='', flush=True)
return input()
'''
!! OVERRIDE ON INHERITANCE !!
'''
def _style_prompt(self, prompt: str) -> str:
return prompt
def __get_prompt(self) -> str:
return self._style_prompt(self._prompt)
def set_prompt(self, prompt: str) -> None:
self._prompt = prompt

234
sandbox/cli/style.py Normal file
View file

@ -0,0 +1,234 @@
'''
=== ANSI Escape Sequences ===
This file exists to organise and name
all important ANSI escape sequences
for management of the CLI.
'''
from sys import stdout
from enum import StrEnum
# Removes ALL set colors/styling
RESET = '\x1b[0m'
'''
Implements text foreground coloring.
'''
class Color(StrEnum):
BLACK = '\x1b[30m'
RED = '\x1b[31m'
GREEN = '\x1b[32m'
YELLOW = '\x1b[33m'
BLUE = '\x1b[34m'
MAGENTA = '\x1b[35m'
CYAN = '\x1b[36m'
WHITE = '\x1b[37m'
'''
Handles the application of a Color to a given text.
Unset the `temp` flag to leave this style on (after the given text)
'''
@staticmethod
def apply(color: 'Color', text: str, temp: bool = True) -> str:
return color + text + Color.WHITE if temp else ''
'''
Implements text effects.
'''
class Effect(StrEnum):
BOLD = '\x1b[1m'
DIM = '\x1b[2m'
ITALICS = '\x1b[3m'
UNDERLINE = '\x1b[4m'
BLINK = '\x1b[5m'
REVERSE = '\x1b[7m'
HIDE = '\x1b[8m'
'''
Returns the opposite ESC code to a given Effect.
ie the opposite of BOLD '\x1b[1m]' is '\x1b[21m'
'''
@staticmethod
def _inverse(effect: 'Effect') -> str:
return f'{effect[:2]}2{effect[2:]}'
'''
Handles the application of a Effect to a given text.
Unset the `temp` flag to leave this style on (after the given text)
'''
@staticmethod
def apply(effect: 'Effect', text: str, temp: bool = True) -> str:
return effect + text + Effect._inverse(effect) if temp else ''
'''
Applies styling (color/effects/etc) to a given string
Unset the `temp` flag to leave this style on (after the given text)
'''
def style(text: str, *args: Color | Effect, temp: bool = True) -> str:
# unsures we don't add redundant "set white" commands
unset_color = False
for arg in args:
unset = temp
if isinstance(arg, Color):
unset_color |= temp
unset = False
text = type(arg).apply(arg, text, temp=True)
if unset_color:
text += Color.WHITE
return text
'''
Implements cursor movement functionality.
NOTE:
The Cursor class currently has no ability
to handle EXACT line (row) numbers. Currently
I'm assuming all functionality of noether can
be implemented solely via relative movements.
'''
class Cursor(StrEnum):
# SAVE current / RESTORE last saved cursor position
_SAVE = f'\x1b[7'
_RESTORE = f'\x1b[8'
_MV_UP = 'A'
_MV_DOWN = 'B'
_MV_LEFT = 'C'
_MV_RIGHT = 'D'
# NEXT/PREV are the same as DOWN/UP (respectively)
# except that the cursor will reset to column 0
_MV_NEXT = 'E'
_MV_PREV = 'F'
# move cursor to the start of the current line
_MV_START = '\r' # there is no ESC CSI for carriage return
_MV_COLUMN = 'G'
'''
Generates an ESC code sequence corresponding
to a horizontal movement relative to the
cursor's current vertical position.
+ive = right
-ive = left
'''
@staticmethod
def _XMOVE_REL(n: int) -> str:
if n > 0:
return f'\x1b[{n}{Cursor._MV_RIGHT}'
if n < 0:
return f'\x1b[{n}{Cursor._MV_LEFT}'
return ''
'''
Generates an ESC code sequence corresponding
to a horizontal movement to an EXACT column.
'''
@staticmethod
def _XMOVE(n: int) -> str:
return f'\x1b[{n}{Cursor._MV_COLUMN}'
'''
Generates an ESC code sequence corresponding
to a vertical movement relative to the
cursor's current vertical position.
+ive = down
-ive = up
'''
@staticmethod
def _YMOVE_REL(n: int, reset: bool = False) -> str:
if n > 0:
if reset:
return f'\x1b[{n}{Cursor._MV_NEXT}'
return f'\x1b[{n}{Cursor._MV_DOWN}'
if n < 0:
if reset:
return f'\x1b[{n}{Cursor._MV_PREV}'
return f'\x1b[{n}{Cursor._MV_UP}'
return ''
'''
Sets the cursor column (horizontal) position to an exact value.
NOTE: does NOT flush stdout buffer
'''
@staticmethod
def set_x(n: int) -> None:
stdout.write(Cursor.XMOVE(n))
'''
Moves the cursor left/right n columns (relative).
NOTE: does NOT flush stdout buffer
'''
@staticmethod
def move_x(n: int) -> None:
stdout.write(Cursor._XMOVE_REL(n))
'''
Moves the cursor up/down n rows and resets the
cursor to be at the start of the line
NOTE: does NOT flush stdout buffer
'''
@staticmethod
def move_y(n: int, reset: bool = True) -> None:
stdout.write(Cursor._YMOVE_REL(n, reset=reset))
'''
Saves the current cursor position.
NOTE: does NOT flush stdout buffer
'''
@staticmethod
def save() -> None:
stdout.write(Cursor._SAVE)
'''
Restores the cursor position to a saved position.
NOTE: does NOT flush stdout buffer
'''
@staticmethod
def restore() -> None:
stdout.write(Cursor._RESTORE)
'''
Handles erasing content displayed on the screen.
NOTE that the cursor position is NOT updated
via these sequences. The \r code should be given after.
'''
class Erase(StrEnum):
# erase everything from the current cursor
# position to the START/END of the screen
_SCR_AFTER = '\x1b[0J'
_SCR_BEFORE = '\x1b[1J'
_SCR_ALL = '\x1b[3J' # erase screen and delete all saved cursors
# erase everything from the current cursor
# position to the START/END of the current line
_LINE_AFTER = '\x1b[0K'
_LINE_BEFORE = '\x1b[1K'
_LINE_ALL = '\x1b[2K'
'''
Erase characters on the entire screen.
Set `before` flag to only erase before the cursor,
set `after` flag to only erase after the cursor.
'''
@staticmethod
def screen(before: bool = False, after: bool = False) -> None:
if before:
stdout.write(Erase._SCR_BEFORE)
elif after:
stdout.write(Erase._SCR_AFTER)
else:
stdout.write(Erase._SCR_ALL)
'''
Erase characters on the current line.
Set `before` flag to only erase before the cursor,
set `after` flag to only erase after the cursor.
'''
@staticmethod
def line(before: bool = False, after: bool = False) -> None:
if before:
stdout.write(Erase._LINE_BEFORE)
elif after:
stdout.write(Erase._LINE_AFTER)
else:
stdout.write(Erase._LINE_ALL)

0
sandbox/cmd/__init__.py Normal file
View file

60
sandbox/cmd/cycsub.py Normal file
View file

@ -0,0 +1,60 @@
from sys import stdout
from noether.cli.style import *
from noether.cli.prompt import *
from noether.lib.structs import Result
from noether.lib.util import digits
from noether.lib.groups import cyclic_subgrp
from noether.lib.primes import totient, is_prime
class cycsub(Prompt):
DEFAULT_PROMPT = style('[n]: ', Color.BLUE)
def __init__(self, ignore_zero: bool = True) -> None:
super().__init__()
self.ignore_zero = ignore_zero
def _parse(self, command: str) -> int:
try:
return Result.succeed(int(command))
except ValueError:
return Result.fail('Not an integer.')
def _exec(self, n: int) -> None:
phi = totient(n)
lpadding = digits(n)
rpadding = digits(phi)
if is_prime(n, phi=n):
Cursor.save()
Cursor.move_y(-1, reset=False)
Cursor.set_x(len(self.DEFAULT_PROMPT) + lpadding + 1)
stdout.write(style('[PRIME]', Color.MAGENTA, Effect.BOLD))
Cursor.restore()
stdout.flush()
# keeps track of all primitive roots
# (note that there will be exactly totient(phi) of them)
proots = []
for g in range(n):
G, order, periodic = cyclic_subgrp(g, n, ignore_zero=self.ignore_zero)
primitive = (order == phi) # primitive root
lpad = ' ' * (lpadding - digits(a))
rpad = ' ' * (rpadding - digits(order))
color_g = Color.RED
style_G = []
style_order = []
if primitive:
color_g = Color.GREEN
style_G = [Color.GREEN, Effect.BOLD]
style_order = [Color.YELLOW, Effect.BOLD]
proots.append(g)
elif gcd(g, n) == 1:
color_g = Color.Yellow
line = style(f'{lpad}{g}', color_g, Effect.BOLD) + \
'-> ' + \
style(f'{order}{rpad} ', *style_order) + \
'| ' + \
style(str(G), *style_G)
print(line, flush=True)

3
sandbox/exceptions.py Normal file
View file

@ -0,0 +1,3 @@
class PromptParseError(Exception):
def __init__(self, message: str) -> None:
super().__init__(message)