Source code for src.py416.general

'''
| Author:  Ezio416
| Created: 2022-08-18
| Updated: 2022-11-21

- Functions for various things
- These are all imported to py416 directly, so just call them like so: :func:`py416.timestamp`
'''
from datetime import datetime as dt
from email.message import EmailMessage
from inspect import currentframe
from re import findall
from smtplib import SMTP_SSL
from ssl import create_default_context

from .variables import SEC_D, SEC_H, SEC_M


[docs]def bytesize(byte: int, si: bool = False) -> str: ''' - scales a number of bytes to a prefixed format Parameters ---------- byte: int - number of bytes to scale si: bool - whether to use SI units (1000^x bytes: KB, MB, GB) - default: False (1024^x bytes: KiB, MiB, GiB) Returns ------- str - bytes in prefixed format, rounded to 2 decimal places - i.e.: - 1200000 => '1.20MB' - 1253656678 => '1.17GiB' ''' byte = int(byte) if byte < 0: raise ValueError(f'bytes can\'t be negative; invalid: {byte}') if si: factor = 1000 units = ('', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') else: factor = 1024 units = ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi') if byte < factor: # less than 1000 or 1024 bytes return f'{byte}B' for unit in units: if byte < factor: return f'{byte:.2f}{unit}B' byte /= factor return f'{byte*factor:.2f}{unit}B' # user passed in stupidly large number
[docs]def emailsmtp(sender_email: str, recipient: str, subject: str, body: str, smtp_server: str, smtp_port: int, password: str, sender_name: str = '') -> bool: ''' - sends an email via SMTP Parameters ---------- sender_email: str - email address to send from recipient: str - email address to send to subject: str - text to put in the email subject body: str - test to put in the email body smtp_server: str - host name for SMTP server smtp_port: int - port for SMTP server password: str - password for sender email sender_name: str - name to show in the 'from' field - default: none Returns ------- bool - whether send was successful ''' msg = EmailMessage() if sender_name: msg['From'] = f'{sender_name} <{sender_email}>' else: msg['From'] = sender_email msg['To'] = recipient msg['Subject'] = subject msg.set_content(body) try: with SMTP_SSL(host=smtp_server, port=smtp_port, context=create_default_context()) as server: server.login(sender_email, password) server.send_message(msg) return True except Exception: return False
[docs]def gettype(thing) -> str: ''' - gets the type of an object - wraps `type() <https://docs.python.org/3/library/functions.html#type>`_ Parameters ---------- thing - object of any type Returns ------- str - type of object ''' return str(type(thing)).split("'")[1]
[docs]def lineno() -> int: ''' - gets the line number in the file where the function was called from Returns ------- int - line number in Python script ''' return currentframe().f_back.f_lineno
[docs]def month2num(month_word: str) -> str: ''' - converts month names to their 2-digit number Parameters ---------- month_word: str - full month name Returns ------- str - zero-padded 2-digit number ''' if type(month_word) is not str: raise TypeError(f'input must be a string; invalid: {month_word}') month_list = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] mydict = {} for i, month in enumerate(month_list, 1): mydict[month] = str(i).zfill(2) try: return mydict[month_word.lower()] except KeyError: return ''
[docs]def pprint(iterable: object, do_print: bool = True, level: int = 0) -> str: ''' - pretty print an iterable object without splitting strings - not yet working for dictionaries or special iterable types Parameters ---------- iterable: object - thing to pretty print do_print: bool - whether to print the output - default: True level: int - recursion level - not meant to be changed (not useful to the user anyway) Returns ------- str - output ''' output = ' ' * level if (typ := type(iterable)) not in (list, set, tuple): return output + (f"'{iterable}'" if typ is str else str(iterable)) if typ is list: open, close = '[', ']' elif typ is set: open, close = '{', '}' elif typ is tuple: open, close = '(', ')' output += open for i, item in enumerate(iterable): output += pprint(item, do_print=False, level=(level + 1 if i else 0)) + ',\n' output = output.rstrip(',\n') + close if do_print: print(output) return output
[docs]def secmod(seconds: float, sep: str = '') -> tuple: ''' - formats a number of seconds nicely, split by days, hours, minutes, and seconds - takes the absolute value of the input so the result is always positive Parameters ---------- seconds: int | float - number to convert sep: str - separator between values - default: nothing Returns ------- tuple [str | int] - string in format and individual values - i.e. ('04d16h47m09s', 9, 47, 16, 4) ''' seconds = abs(int(seconds)) if type(sep) is not str: raise ValueError(f'input must be a string; invalid: {sep}') if not seconds: return ['0s', 0, 0, 0, 0] result = '' m, s = divmod(seconds, 60) h, m = divmod(m, 60) d, h = divmod(h, 24) def zf(var): return str(var).zfill(2) if d: result += zf(d) + 'd' + sep if h: result += zf(h) + 'h' + sep if m: result += zf(m) + 'm' + sep if s: result += zf(s) + 's' result = result.rstrip(sep) return result, s, m, h, d
[docs]def secmod_inverse(timestr: str) -> int: ''' - does essentially the opposite of :func::`secmod` - converts a string to a number of seconds Parameters ---------- timestr: str - representation of time - must be formatted like the base output from :func::`secmod`, i.e. "3d16h5m47s" - can be missing parts, i.e. "3s47s" - capitalization is ignored - if multiple of the same type of value are passed in the string, i.e. "4h16h", only the first value is grabbed Returns ------- int - number of seconds ''' if type(timestr) is not str: raise TypeError(f'input must be a string; invalid: {timestr}') timestr = timestr.lower() dy = findall(r'[\d]{1,}[d]{1}', timestr) dy = int(dy[0].split('d')[0]) if dy else 0 hr = findall(r'[\d]{1,}[h]{1}', timestr) hr = int(hr[0].split('h')[0]) if hr else 0 mn = findall(r'[\d]{1,}[m]{1}', timestr) mn = int(mn[0].split('m')[0]) if mn else 0 sc = findall(r'[\d]{1,}[s]{1}', timestr) sc = int(sc[0].split('s')[0]) if sc else 0 return dy * SEC_D + hr * SEC_H + mn * SEC_M + sc
[docs]def timestamp(brackets: bool = True, micro: bool = False, offset: bool = True, readable: bool = False, seconds: bool = True, utc: bool = False) -> str: ''' - creates a timestamp in ISO format with additional formatting - default example: [2022-07-06T13:57:12-06:00] Parameters ---------- brackets: bool - whether to surround timestamp in square brackets - default: True micro: bool - whether to include microseconds - default: False offset: bool - whether to include offset from UTC, e.g. timezone - default: True readable: bool - whether to internal whitespace for legibility - default: False seconds: bool - whether to include seconds - default: True utc: bool - current UTC time - default: False Returns ------- str - current timestamp with chosen formatting - i.e. [2022-08-18 07:15:43.962 +00:00] ''' brackets, micro, offset, readable, seconds, utc = (bool(flag) for flag in (brackets, micro, offset, readable, seconds, utc)) if utc: now = dt.utcnow() offset_val = '+00:00' else: now = dt.now() offset_val = str(now.astimezone())[-6:] if not micro: now = now.replace(microsecond=0) now = now.isoformat() if not seconds: now = now[:-3] if readable: now = now.replace('T', ' ') if offset: now += ' ' if offset: now += offset_val if brackets: now = f'[{now}]' return now.strip()
[docs]def unpack(iterable) -> tuple: ''' - recursively retrieves items from some iterable types Parameters ---------- iterable: list | tuple - thing to unpack Returns ------- tuple - all retrieved items - if `iterable` is not actually iterable, returns a tuple with just the item ''' iterables = (list, tuple) if type(iterable) not in iterables: return iterable, values = [] for item in iterable: if type(item) not in iterables: values.append(item) else: values += list(unpack(item)) return tuple(values)