summaryrefslogtreecommitdiff
path: root/game/python-extra/utils
diff options
context:
space:
mode:
authorJesusaves <cpntb1@ymail.com>2020-12-09 13:32:01 -0300
committerJesusaves <cpntb1@ymail.com>2020-12-09 13:32:01 -0300
commit63afe4145f410a844c647d4e3f1059f568175c1e (patch)
tree15da6a890c78d73370f44f9fd5d59badfbbe60e4 /game/python-extra/utils
downloadclient-init.tar.gz
client-init.tar.bz2
client-init.tar.xz
client-init.zip
Initial commit, forked from Spheresinit
Diffstat (limited to 'game/python-extra/utils')
-rw-r--r--game/python-extra/utils/__init__.py0
-rw-r--r--game/python-extra/utils/bools.py7
-rw-r--r--game/python-extra/utils/dates.py225
-rw-r--r--game/python-extra/utils/dicts/__init__.py7
-rw-r--r--game/python-extra/utils/dicts/chained_dict.py71
-rw-r--r--game/python-extra/utils/dicts/helpers.py99
-rw-r--r--game/python-extra/utils/dicts/limited_dict.py40
-rw-r--r--game/python-extra/utils/enum.py132
-rw-r--r--game/python-extra/utils/lists.py42
-rw-r--r--game/python-extra/utils/math.py16
-rw-r--r--game/python-extra/utils/objects.py59
11 files changed, 698 insertions, 0 deletions
diff --git a/game/python-extra/utils/__init__.py b/game/python-extra/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/game/python-extra/utils/__init__.py
diff --git a/game/python-extra/utils/bools.py b/game/python-extra/utils/bools.py
new file mode 100644
index 0000000..f2bcab5
--- /dev/null
+++ b/game/python-extra/utils/bools.py
@@ -0,0 +1,7 @@
+try:
+ reduce
+except NameError:
+ from functools import reduce
+
+def xor(*things):
+ return reduce(lambda x, y: bool(x) ^ bool(y), things)
diff --git a/game/python-extra/utils/dates.py b/game/python-extra/utils/dates.py
new file mode 100644
index 0000000..f5643cf
--- /dev/null
+++ b/game/python-extra/utils/dates.py
@@ -0,0 +1,225 @@
+"""Useful things to do with dates"""
+import datetime
+
+
+def date_from_string(string, format_string=None):
+ """Runs through a few common string formats for datetimes,
+ and attempts to coerce them into a datetime. Alternatively,
+ format_string can provide either a single string to attempt
+ or an iterable of strings to attempt."""
+
+ if isinstance(format_string, str):
+ return datetime.datetime.strptime(string, format_string).date()
+
+ elif format_string is None:
+ format_string = [
+ "%Y-%m-%d",
+ "%m-%d-%Y",
+ "%m/%d/%Y",
+ "%d/%m/%Y",
+ ]
+
+ for format in format_string:
+ try:
+ return datetime.datetime.strptime(string, format).date()
+ except ValueError:
+ continue
+
+ raise ValueError("Could not produce date from string: {}".format(string))
+
+
+def to_datetime(plain_date, hours=0, minutes=0, seconds=0, ms=0):
+ """given a datetime.date, gives back a datetime.datetime"""
+ # don't mess with datetimes
+ if isinstance(plain_date, datetime.datetime):
+ return plain_date
+ return datetime.datetime(
+ plain_date.year,
+ plain_date.month,
+ plain_date.day,
+ hours,
+ minutes,
+ seconds,
+ ms,
+ )
+
+
+class TimePeriod(object):
+
+ def __init__(self, earliest, latest):
+ if not isinstance(earliest, datetime.date) and earliest is not None:
+ raise TypeError("Earliest must be a date or None")
+ if not isinstance(latest, datetime.date) and latest is not None:
+ raise TypeError("Latest must be a date or None")
+
+ # convert dates to datetimes, for to have better resolution
+ if earliest is not None:
+ earliest = to_datetime(earliest)
+ if latest is not None:
+ latest = to_datetime(latest, 23, 59, 59)
+
+ if earliest is not None and latest is not None and earliest >= latest:
+ raise ValueError("Earliest must be earlier than latest")
+
+ self._earliest = earliest
+ self._latest = latest
+
+ def __contains__(self, key):
+ if isinstance(key, datetime.date):
+ key = to_datetime(key)
+
+ if self._latest is None:
+ upper_bounded = True
+ else:
+ upper_bounded = key <= self._latest
+
+ if self._earliest is None:
+ lower_bounded = True
+ else:
+ lower_bounded = self._earliest <= key
+
+ return upper_bounded and lower_bounded
+
+ elif isinstance(key, TimePeriod):
+ if self._latest is None:
+ upper_bounded = True
+ elif key._latest is None:
+ upper_bounded = False
+ else:
+ upper_bounded = self._latest >= key._latest
+
+ if self._earliest is None:
+ lower_bounded = True
+ elif key._earliest is None:
+ lower_bounded = False
+ else:
+ lower_bounded = self._earliest <= key._earliest
+
+ return upper_bounded and lower_bounded
+
+ def contains(self, other):
+ return other in self
+
+ def overlaps(self, other):
+ """does another datetime overlap with this one? this is a symmetric
+ property.
+
+ TP1 |------------|
+ -------------------------------------------------> time
+ TP2 |--------------|
+
+ TP1.overlaps(TP2) == TP2.overlaps(TP1) == True
+
+ args:
+ other - a TimePeriod
+ """
+
+ return self._latest in other or self._earliest in other
+
+ def __eq__(self, other):
+ return (self._earliest == other._earliest) and (self._latest == other._latest)
+
+ def __hash__(self):
+ return hash((self._earliest, self._latest))
+
+ def __repr__(self):
+ return "<{}: {}-{}>".format(
+ self.__class__.__name__,
+ self._earliest,
+ self._latest,
+ )
+
+ @classmethod
+ def get_containing_period(cls, *periods):
+ """Given a bunch of TimePeriods, return a TimePeriod that most closely
+ contains them."""
+
+ if any(not isinstance(period, TimePeriod) for period in periods):
+ raise TypeError("periods must all be TimePeriods: {}".format(periods))
+
+ latest = datetime.datetime.min
+ earliest = datetime.datetime.max
+
+ for period in periods:
+ # the best we can do to conain None is None!
+ if period._latest is None:
+ latest = None
+ elif latest is not None and period._latest > latest:
+ latest = period._latest
+
+ if period._earliest is None:
+ earliest = None
+ elif earliest is not None and period._earliest < earliest:
+ earliest = period._earliest
+
+ return TimePeriod(earliest, latest)
+
+
+class DiscontinuousTimePeriod(object):
+ """A bunch of TimePeriods"""
+
+ def __init__(self, *periods):
+ if any(not isinstance(period, TimePeriod) for period in periods):
+ raise TypeError("periods must all be TimePeriods: {}".format(periods))
+
+ periods = set(periods)
+
+ no_overlaps_periods = []
+ for period in periods:
+ for other_period in periods:
+ if id(other_period) == id(period):
+ continue
+
+ # periods that overlap should be combined
+ if period.overlaps(other_period):
+ period = TimePeriod.get_containing_period(period, other_period)
+
+ no_overlaps_periods.append(period)
+
+ no_equals_periods = []
+ reference = set(no_overlaps_periods)
+ for period in no_overlaps_periods:
+ # clean out duplicated periods
+ if any(other_period == period and other_period is not period for other_period in reference):
+ reference.remove(period)
+ else:
+ no_equals_periods.append(period)
+
+ no_contains_periods = []
+ for period in no_equals_periods:
+ # don't need to keep periods that are wholly contained
+ skip = False
+ for other_period in no_equals_periods:
+ if id(other_period) == id(period):
+ continue
+
+ if period in other_period:
+ skip = True
+
+ if not skip:
+ no_contains_periods.append(period)
+ self._periods = no_contains_periods
+
+ def __contains__(self, other):
+ if isinstance(other, (datetime.date, TimePeriod)):
+ for period in self._periods:
+ if other in period:
+ return True
+
+
+def days_ago(days, give_datetime=True):
+ delta = datetime.timedelta(days=days)
+ dt = datetime.datetime.now() - delta
+ if give_datetime:
+ return dt
+ else:
+ return dt.date()
+
+
+def days_ahead(days, give_datetime=True):
+ delta = datetime.timedelta(days=days)
+ dt = datetime.datetime.now() + delta
+ if give_datetime:
+ return dt
+ else:
+ return dt.date()
diff --git a/game/python-extra/utils/dicts/__init__.py b/game/python-extra/utils/dicts/__init__.py
new file mode 100644
index 0000000..0ab7c69
--- /dev/null
+++ b/game/python-extra/utils/dicts/__init__.py
@@ -0,0 +1,7 @@
+"""Helper functinos for dealing with dicts.
+
+Things you always wished you could do more succinctly!
+"""
+from .limited_dict import LimitedDict
+from .chained_dict import ChainedDict
+from .helpers import *
diff --git a/game/python-extra/utils/dicts/chained_dict.py b/game/python-extra/utils/dicts/chained_dict.py
new file mode 100644
index 0000000..f1fe36f
--- /dev/null
+++ b/game/python-extra/utils/dicts/chained_dict.py
@@ -0,0 +1,71 @@
+from collections import MutableMapping
+from itertools import chain
+
+
+class ChainedDict(MutableMapping):
+
+ def __init__(self, parent=None, **kwargs):
+ self.__parent = parent
+ self.__deleted_keys = set()
+ self.__data = kwargs
+
+ def __contains__(self, key):
+ if self.__parent is not None:
+ return (
+ (key in self.__data or key in self.__parent)
+ and key not in self.__deleted_keys
+ )
+ return key in self.__data
+
+ def __getitem__(self, key):
+ try:
+ return self.__data[key]
+ except KeyError:
+ if self.__parent is not None and key not in self.__deleted_keys:
+ return self.__parent[key]
+ else:
+ raise
+
+ def __setitem__(self, key, val):
+ self.__data[key] = val
+ self.__deleted_keys.discard(key)
+
+ def __delitem__(self, key):
+ if key in self:
+ self.__deleted_keys.add(key)
+ try:
+ del self.__data[key]
+ except KeyError:
+ pass
+ else:
+ raise KeyError(key)
+
+ def __repr__(self):
+ return "{}({})".format(self.__class__.__name__, dict(self.items()))
+
+ def __iter__(self):
+ return self.keys()
+
+ def __len__(self):
+ return len(list(self.keys()))
+
+ def iterkeys(self):
+ yielded = set(self.__deleted_keys)
+ if self.__parent is None:
+ iterable = self.__data.keys()
+ else:
+ iterable = chain(self.__parent.keys(), self.__data.keys())
+
+ for key in iterable:
+ if key in yielded:
+ continue
+ yield key
+ yielded.add(key)
+
+ keys = iterkeys
+
+ def iteritems(self):
+ for key in self.iterkeys():
+ yield key, self[key]
+
+ items = iteritems
diff --git a/game/python-extra/utils/dicts/helpers.py b/game/python-extra/utils/dicts/helpers.py
new file mode 100644
index 0000000..8b1f594
--- /dev/null
+++ b/game/python-extra/utils/dicts/helpers.py
@@ -0,0 +1,99 @@
+from collections import namedtuple
+
+
+def from_keyed_iterable(iterable, key, filter_func=None):
+ """Construct a dictionary out of an iterable, using an attribute name as
+ the key. Optionally provide a filter function, to determine what should be
+ kept in the dictionary."""
+
+ generated = {}
+
+ for element in iterable:
+ try:
+ k = getattr(element, key)
+ except AttributeError:
+ raise RuntimeError("{} does not have the keyed attribute: {}".format(
+ element, key
+ ))
+
+ if filter_func is None or filter_func(element):
+ if k in generated:
+ generated[k] += [element]
+ else:
+ generated[k] = [element]
+
+ return generated
+
+
+def subtract_by_key(dict_a, dict_b):
+ """given two dicts, a and b, this function returns c = a - b, where
+ a - b is defined as the key difference between a and b.
+
+ e.g.,
+ {1:None, 2:3, 3:"yellow", 4:True} - {2:4, 1:"green"} =
+ {3:"yellow", 4:True}
+
+ """
+ difference_dict = {}
+ for key in dict_a:
+ if key not in dict_b:
+ difference_dict[key] = dict_a[key]
+
+ return difference_dict
+
+
+def subtract(dict_a, dict_b, strict=False):
+ """a stricter form of subtract_by_key(), this version will only remove an
+ entry from dict_a if the key is in dict_b *and* the value at that key
+ matches"""
+ if not strict:
+ return subtract_by_key(dict_a, dict_b)
+
+ difference_dict = {}
+ for key in dict_a:
+ if key not in dict_b or dict_b[key] != dict_a[key]:
+ difference_dict[key] = dict_a[key]
+
+ return difference_dict
+
+
+WinnowedResult = namedtuple("WinnowedResult", ['has', 'has_not'])
+def winnow_by_keys(dct, keys=None, filter_func=None):
+ """separates a dict into has-keys and not-has-keys pairs, using either
+ a list of keys or a filtering function."""
+ has = {}
+ has_not = {}
+
+ for key in dct:
+ key_passes_check = False
+ if keys is not None:
+ key_passes_check = key in keys
+ elif filter_func is not None:
+ key_passes_check = filter_func(key)
+
+ if key_passes_check:
+ has[key] = dct[key]
+ else:
+ has_not[key] = dct[key]
+
+ return WinnowedResult(has, has_not)
+
+
+def intersection(dict_a, dict_b, strict=True):
+ intersection_dict = {}
+
+ for key in dict_a:
+ if key in dict_b:
+ if not strict or dict_a[key] == dict_b[key]:
+ intersection_dict[key] = dict_a[key]
+
+ return intersection_dict
+
+
+def setdefaults(dct, defaults):
+ """Given a target dct and a dict of {key:default value} pairs,
+ calls setdefault for all of those pairs."""
+ for key in defaults:
+ dct.setdefault(key, defaults[key])
+
+ return dct
diff --git a/game/python-extra/utils/dicts/limited_dict.py b/game/python-extra/utils/dicts/limited_dict.py
new file mode 100644
index 0000000..c690118
--- /dev/null
+++ b/game/python-extra/utils/dicts/limited_dict.py
@@ -0,0 +1,40 @@
+from collections import MutableMapping
+
+
+class LimitedDict(MutableMapping):
+ def __init__(self, args=None, **kwargs):
+ keys = kwargs.pop('keys', [])
+ self.__keys = keys
+
+ self.__data = {}
+
+ if args:
+ kwargs.update((key, val) for key, val in args)
+
+ for key, val in kwargs.items():
+ self[key] = val
+
+ def __setitem__(self, key, val):
+ if key not in self.__keys:
+ raise KeyError("Illegal key: {}".format(key))
+
+ self.__data[key] = val
+
+ def __getitem__(self, key):
+ return self.__data[key]
+
+ def __iter__(self):
+ return self.__data.__iter__()
+
+ def __delitem__(self, key):
+ del self.__data[key]
+
+ def __len__(self):
+ return len(self.__data)
+
+ def __repr__(self):
+ return "{}({}, {})".format(self.__class__.__name__, self.defined_keys, self.__data)
+
+ @property
+ def defined_keys(self):
+ return self.__keys
diff --git a/game/python-extra/utils/enum.py b/game/python-extra/utils/enum.py
new file mode 100644
index 0000000..8b145a4
--- /dev/null
+++ b/game/python-extra/utils/enum.py
@@ -0,0 +1,132 @@
+"""Who hasn't needed a good, old-fashioned enum now and then?"""
+
+
+class _enum(object):
+
+ def __call__(self, enum_name, *args, **kwargs):
+ if args and kwargs:
+ raise TypeError("enums can only be made from args XOR kwargs")
+
+ enum_items = {}
+
+ counter = 0
+ for name, val in kwargs.items():
+ if val is None:
+ val = counter
+ counter += 1
+ elif isinstance(val, int):
+ counter = val + 1
+
+ enum_items[name] = val
+
+ for val, name in enumerate(args, start=counter):
+ enum_items[name] = val
+
+ return type(enum_name, (Enum,), enum_items)
+
+ def from_iterable(self, iterable):
+ return self(*iterable)
+
+ def from_dict(self, dct):
+ return self(**dct)
+
+ def __iter__(self):
+ for k, v in self.__enum_items.items():
+ yield k, v
+
+ def __repr__(self):
+ return "<{}: {}>".format(self.__class__.__name__, self.__enum_items.values())
+enum = _enum()
+
+
+class EnumItem(object):
+
+ def __init__(self, parent, name, value):
+ self.__parent = parent
+ self.__name = name
+ self.__value = value
+
+ def __repr__(self):
+ return "<{}: {} [{}]>".format(self.__class__.__name__, self.name, self.value)
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ if self.parent.is_strict and self.parent != other.parent:
+ raise ValueError("can't compare EnumItems from different enums")
+ return self.value == other.value
+
+ return self.value == other
+
+ @property
+ def value(self):
+ return self.__value
+
+ @property
+ def name(self):
+ return self.__name
+
+ @property
+ def parent(self):
+ return self.__parent
+
+
+class _EnumMeta(type):
+ def __new__(cls, name, bases, attr_dict):
+
+ options = attr_dict.pop('Options', object)
+
+ attr_dict['__strict__'] = getattr(options, "strict_compare", True)
+
+ new_enum = super(_EnumMeta, cls).__new__(cls, name, bases, {})
+
+ enum_items = {}
+
+ for attr_name, attr_value in attr_dict.items():
+ if attr_name.startswith('__'):
+ super(_EnumMeta, cls).__setattr__(new_enum, attr_name, attr_value)
+ continue
+
+ if getattr(options, 'force_uppercase', False):
+ attr_dict.pop(attr_name)
+ attr_name = attr_name.upper()
+
+ enum_item = EnumItem(new_enum, attr_name, attr_value)
+
+ enum_items[attr_name] = enum_item
+ super(_EnumMeta, cls).__setattr__(new_enum, attr_name, enum_item)
+
+ if getattr(options, "frozen", True):
+ super(_EnumMeta, cls).__setattr__(new_enum, '__frozen__', True)
+ else:
+ super(_EnumMeta, cls).__setattr__(new_enum, '__frozen__', False)
+
+ if getattr(options, "strict", False):
+ super(_EnumMeta, cls).__setattr__(new_enum, '__strict__', True)
+ else:
+ super(_EnumMeta, cls).__setattr__(new_enum, '__strict__', False)
+
+ super(_EnumMeta, cls).__setattr__(new_enum, '__enum_item_map__', enum_items)
+
+ return new_enum
+
+ def __setattr__(cls, name, val):
+ if getattr(cls, "__frozen__", False):
+ raise TypeError("can't set attributes on a frozen enum")
+
+ if name in cls.__enum_item_map__:
+ val = EnumItem(cls, name, val)
+ cls.__enum_item_map__[name] = val
+
+ super(_EnumMeta, cls).__setattr__(name, val)
+
+ @property
+ def is_strict(cls):
+ return getattr(cls, "__strict__", True)
+
+ def get_name_value_map(cls):
+ e = cls.__enum_item_map__
+ return dict((e[i].name, e[i].value) for i in e)
+
+
+class Enum(_EnumMeta("EnumBase", (object, ), {})):
+ pass
diff --git a/game/python-extra/utils/lists.py b/game/python-extra/utils/lists.py
new file mode 100644
index 0000000..aacffdb
--- /dev/null
+++ b/game/python-extra/utils/lists.py
@@ -0,0 +1,42 @@
+"""List-related functions"""
+
+
+def unlist(list_thing, complain=True):
+ """transforms [Something] -> Something. By default, raises a ValueError for
+ any other list values."""
+ if complain and len(list_thing) > 1:
+ raise ValueError("More than one element in {}".format(list_thing))
+ elif len(list_thing) == 1:
+ return list_thing[0]
+
+ if complain:
+ raise ValueError("Nothing in {}".format(list_thing))
+ return None
+
+
+def flatten(iterable):
+ """Fully flattens an iterable:
+ In: flatten([1,2,3,4,[5,6,[7,8]]])
+ Out: [1,2,3,4,5,6,7,8]
+ """
+ container = iterable.__class__
+
+ placeholder = []
+ for item in iterable:
+ try:
+ placeholder.extend(flatten(item))
+ except TypeError:
+ placeholder.append(item)
+
+ return container(placeholder)
+
+
+def flat_map(iterable, func):
+ """func must take an item and return an interable that contains that
+ item. this is flatmap in the classic mode"""
+ results = []
+ for element in iterable:
+ result = func(element)
+ if len(result) > 0:
+ results.extend(result)
+ return results
diff --git a/game/python-extra/utils/math.py b/game/python-extra/utils/math.py
new file mode 100644
index 0000000..eaae187
--- /dev/null
+++ b/game/python-extra/utils/math.py
@@ -0,0 +1,16 @@
+import collections
+import operator
+
+# py3 doesn't include reduce as a builtin
+try:
+ reduce
+except NameError:
+ from functools import reduce
+
+
+def product(sequence, initial=1):
+ """like the built-in sum, but for multiplication."""
+ if not isinstance(sequence, collections.Iterable):
+ raise TypeError("'{}' object is not iterable".format(type(sequence).__name__))
+
+ return reduce(operator.mul, sequence, initial)
diff --git a/game/python-extra/utils/objects.py b/game/python-extra/utils/objects.py
new file mode 100644
index 0000000..f22efcd
--- /dev/null
+++ b/game/python-extra/utils/objects.py
@@ -0,0 +1,59 @@
+_get_attr_raise_on_attribute_error = "RAISE ON EXCEPTION"
+
+def get_attr(obj, string_rep, default=_get_attr_raise_on_attribute_error, separator="."):
+ """ getattr via a chain of attributes like so:
+ >>> import datetime
+ >>> some_date = datetime.date.today()
+ >>> get_attr(some_date, "month.numerator.__doc__")
+ 'int(x[, base]) -> integer\n\nConvert a string or number to an integer, ...
+ """
+ attribute_chain = string_rep.split(separator)
+
+ current_obj = obj
+
+ for attr in attribute_chain:
+ try:
+ current_obj = getattr(current_obj, attr)
+ except AttributeError:
+ if default is _get_attr_raise_on_attribute_error:
+ raise AttributeError(
+ "Bad attribute \"{}\" in chain: \"{}\"".format(attr, string_rep)
+ )
+ return default
+
+ return current_obj
+
+
+class ImmutableWrapper(object):
+ _obj = None
+ _recursive = None
+
+ def __init__(self, obj, recursive):
+ self._obj = obj
+ self._recursive = recursive
+
+ def __setattr__(self, name, val):
+ if name == "_obj" and self._obj is None:
+ object.__setattr__(self, name, val)
+ return
+ elif name == "_recursive" and self._recursive is None:
+ object.__setattr__(self, name, val)
+ return
+
+ raise AttributeError("This object has been marked as immutable; you cannot set its attributes.")
+
+ def __getattr__(self, name):
+ if self._recursive:
+ return immutable(getattr(self._obj, name), recursive=self._recursive)
+
+ return getattr(self._obj, name)
+
+ def __repr__(self):
+ return "<Immutable {}: {}>".format(self._obj.__class__.__name__, self._obj.__repr__())
+
+
+def immutable(obj, recursive=True):
+ """wraps the argument in a pass-through class that disallows all attribute
+ setting. If the `recursive` flag is true, all attribute accesses will
+ return an immutable-wrapped version of the "real" attribute."""
+ return ImmutableWrapper(obj, recursive)