init
This commit is contained in:
commit
09d4c52043
19 changed files with 542 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
__pycache__/
|
||||
1
README
Normal file
1
README
Normal file
|
|
@ -0,0 +1 @@
|
|||
The "imbaud python library" (imp lib), or just imp for short!
|
||||
0
imp/__init__.py
Normal file
0
imp/__init__.py
Normal file
19
imp/constants.py
Normal file
19
imp/constants.py
Normal 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
0
imp/crypto/__init__.py
Normal file
32
imp/crypto/cyphers.py
Normal file
32
imp/crypto/cyphers.py
Normal 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
0
imp/math/__init__.py
Normal file
37
imp/math/groups.py
Normal file
37
imp/math/groups.py
Normal 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
67
imp/math/primes.py
Normal 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
2
imp/math/util.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
def digits(n: int) -> int:
|
||||
return len(str(n))
|
||||
1
imp/structs/__init__.py
Normal file
1
imp/structs/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from noether.lib.structs.__result import Result
|
||||
19
imp/structs/__result.py
Normal file
19
imp/structs/__result.py
Normal 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
0
sandbox/__init__.py
Normal file
0
sandbox/cli/__init__.py
Normal file
0
sandbox/cli/__init__.py
Normal file
66
sandbox/cli/prompt.py
Normal file
66
sandbox/cli/prompt.py
Normal 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
234
sandbox/cli/style.py
Normal 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
0
sandbox/cmd/__init__.py
Normal file
60
sandbox/cmd/cycsub.py
Normal file
60
sandbox/cmd/cycsub.py
Normal 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
3
sandbox/exceptions.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
class PromptParseError(Exception):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message)
|
||||
Loading…
Add table
Add a link
Reference in a new issue