#!/usr/bin/env python
from setuptools import setup
setup(
  name = 'cs.mappings',
  author = 'Cameron Simpson',
  author_email = 'cs@cskk.id.au',
  version = '20201228',
  url = 'https://bitbucket.org/cameron_simpson/css/commits/all',
  description =
    'Facilities for mappings and objects associated with mappings.',
  long_description =
    ('Facilities for mappings and objects associated with mappings.\n'    
 '\n'    
 '*Latest release 20201228*:\n'    
 'New PrefixedMappingProxy presenting the keys of another mapping commencing '    
 'with a prefix.\n'    
 '\n'    
 'In particular `named_column_tuple(column_names)`,\n'    
 'a function returning a factory\n'    
 'for namedtuples subclasses derived from the supplied column names,\n'    
 'and `named_column_tuples(rows)`,\n'    
 'a function returning a namedtuple factory and an iterable of instances\n'    
 'containing the row data.\n'    
 'These are used by the `csv_import` and `xl_import` functions\n'    
 'from `cs.csvutils`.\n'    
 '\n'    
 '## Class `AttrableMapping(builtins.dict,AttrableMappingMixin)`\n'    
 '\n'    
 'A `dict` subclass using `AttrableMappingMixin`.\n'    
 '\n'    
 '## Class `AttrableMappingMixin`\n'    
 '\n'    
 'Provides a `__getattr__` which accesses the mapping value.\n'    
 '\n'    
 '### Method `AttrableMappingMixin.__getattr__(self, attr)`\n'    
 '\n'    
 'Unknown attributes are obtained from the mapping entries.\n'    
 '\n'    
 '## Class `AttributableList(builtins.list)`\n'    
 '\n'    
 'An `AttributableList` maps unimplemented attributes\n'    
 'onto the list members and returns you a new `AttributableList`\n'    
 'with the results, ready for a further dereference.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 '    >>> class C(object):\n'    
 '    ...   def __init__(self, i):\n'    
 '    ...     self.i = i\n'    
 '    >>> Cs = [ C(1), C(2), C(3) ]\n'    
 '    >>> AL = AttributableList( Cs )\n'    
 '    >>> print(AL.i)\n'    
 '    [1, 2, 3]\n'    
 '\n'    
 '### Method `AttributableList.__init__(self, initlist=None, strict=False)`\n'    
 '\n'    
 'Initialise the list.\n'    
 '\n'    
 'The optional parameter `initlist` initialises the list\n'    
 'as for a normal list.\n'    
 '\n'    
 'The optional parameter `strict`, if true, causes list elements\n'    
 'lacking the attribute to raise an AttributeError. If false,\n'    
 'list elements without the attribute are omitted from the results.\n'    
 '\n'    
 '## Function `dicts_to_namedtuples(dicts, class_name, keys=None)`\n'    
 '\n'    
 'Scan an iterable of `dict`s,\n'    
 'yield a sequence of `namedtuple`s derived from them.\n'    
 '\n'    
 'Parameters:\n'    
 '* `dicts`: the `dict`s to scan and convert, an iterable\n'    
 '* `class_name`: the name for the new `namedtuple` class\n'    
 '* `keys`: optional iterable of `dict` keys of interest;\n'    
 '  if omitted then the `dicts` are scanned in order to learn the keys\n'    
 '\n'    
 'Note that if `keys` is not specified\n'    
 'this generator prescans the `dicts` in order to learn their keys.\n'    
 'As a consequence, all the `dicts` will be kept in memory\n'    
 'and no `namedtuple`s will be yielded until after that prescan completes.\n'    
 '\n'    
 '## Class `FallbackDict(collections.defaultdict,builtins.dict)`\n'    
 '\n'    
 'A dictlike object that inherits from another dictlike object;\n'    
 'this is a convenience subclass of `defaultdict`.\n'    
 '\n'    
 '## Class `JSONableMappingMixin`\n'    
 '\n'    
 'Provide `.from_json()`, `.as_json()` and `.append_ndjson()` methods,\n'    
 'and `__str__=as_json` and a `__repr__`.\n'    
 '\n'    
 '### Method `JSONableMappingMixin.__str__(self)`\n'    
 '\n'    
 'The dict transcribed as JSON.\n'    
 '\n'    
 "If the instance's class has `json_default` or `json_separators` these\n"    
 'are used for the `default` and `separators` parameters of the '    
 '`json.dumps()`\n'    
 'call.\n'    
 "Note that the default value of `separators` is `(',',':')`\n"    
 'which produces the most compact JSON form.\n'    
 '\n'    
 '### Method `JSONableMappingMixin.append_ndjson(arg, *a, **kw)`\n'    
 '\n'    
 'Append this object to `f`, a file or filename, as NDJSON.\n'    
 '\n'    
 '### Method `JSONableMappingMixin.as_json(self)`\n'    
 '\n'    
 'The dict transcribed as JSON.\n'    
 '\n'    
 "If the instance's class has `json_default` or `json_separators` these\n"    
 'are used for the `default` and `separators` parameters of the '    
 '`json.dumps()`\n'    
 'call.\n'    
 "Note that the default value of `separators` is `(',',':')`\n"    
 'which produces the most compact JSON form.\n'    
 '\n'    
 '### Method `JSONableMappingMixin.from_json(js)`\n'    
 '\n'    
 'Prepare an dict from JSON text.\n'    
 '\n'    
 'If the class has `json_object_hook` or `json_object_pairs_hook`\n'    
 'attributes these are used as the `object_hook` and\n'    
 '`object_pairs_hook` parameters respectively of the `json.loads()` call.\n'    
 '\n'    
 '## Class `LoadableMappingMixin`\n'    
 '\n'    
 'A base mixin to provide `.by_`* attributes\n'    
 'which index records from an autoloaded backing store,\n'    
 'which might be a file or might be another related data structure.\n'    
 'The records are themselves key->value mappings, such as `dict`s.\n'    
 '\n'    
 'The primary key name is provided by the `.loadable_mapping_key`\n'    
 'class attribute, to be provided by subclasses.\n'    
 '\n'    
 'Note that this mixin keeps the entire loadable mapping in memory.\n'    
 '\n'    
 'Note that this does not see subsequent changes to loaded records\n'    
 'i.e. changing the value of some record[k]\n'    
 'does not update the index associated with the .by_k attribute.\n'    
 '\n'    
 'Subclasses must provide the following attributes and methods:\n'    
 '* `loadable_mapping_key`: the name of the primary key;\n'    
 '  it is an error for a multiple records to have the same primary key\n'    
 '* `scan_mapping`: a generator method to scan the backing store\n'    
 '  and yield records, used for the inital load of the mapping\n'    
 '* `append_to_mapping(record)`: add a new record to the backing store;\n'    
 '  this is called from the `.add_to_mapping(record)` method\n'    
 '  after indexing to persist the record in the backing store\n'    
 '\n'    
 'See `UUIDNDJSONMapping` and `UUIDedDict` for an example subclass\n'    
 'indexing records from a newline delimited JSON file.\n'    
 '\n'    
 '### Method `LoadableMappingMixin.__len__(self)`\n'    
 '\n'    
 'The length of the primary key mapping.\n'    
 '\n'    
 '### Method `LoadableMappingMixin.add_to_mapping(self, record, '    
 'exists_ok=False)`\n'    
 '\n'    
 'Add a record to the mapping.\n'    
 '\n'    
 'This indexes the record against the various `by_`* indices\n'    
 'and then calls `self.append_to_mapping(record)`\n'    
 'to save the record to the backing store.\n'    
 '\n'    
 '### Method `LoadableMappingMixin.scan_mapping(self)`\n'    
 '\n'    
 'Scan the mapping records (themselves mappings) from the backing store,\n'    
 'which might be a file or another related data structure.\n'    
 'Yield each record as scanned.\n'    
 '\n'    
 '### Property `LoadableMappingMixin.scan_mapping_length`\n'    
 '\n'    
 'The number of records encountered during the backend scan.\n'    
 '\n'    
 '## Class `MappingChain`\n'    
 '\n'    
 'A mapping interface to a sequence of mappings.\n'    
 '\n'    
 'It does not support `__setitem__` at present;\n'    
 'that is expected to be managed via the backing mappings.\n'    
 '\n'    
 '### Method `MappingChain.__init__(self, mappings=None, get_mappings=None)`\n'    
 '\n'    
 'Initialise the MappingChain.\n'    
 '\n'    
 'Parameters:\n'    
 '* `mappings`: initial sequence of mappings, default None.\n'    
 '* `get_mappings`: callable to obtain the initial sequence of\n'    
 '\n'    
 'Exactly one of `mappings` or `get_mappings` must be provided.\n'    
 '\n'    
 '### Method `MappingChain.__getitem__(self, key)`\n'    
 '\n'    
 'Return the first value for `key` found in the mappings.\n'    
 'Raise KeyError if the key in not found in any mapping.\n'    
 '\n'    
 '### Method `MappingChain.get(self, key, default=None)`\n'    
 '\n'    
 'Get the value associated with `key`, return `default` if missing.\n'    
 '\n'    
 '### Method `MappingChain.keys(self)`\n'    
 '\n'    
 'Return the union of the keys in the mappings.\n'    
 '\n'    
 '## Class `MethodicalList(AttributableList,builtins.list)`\n'    
 '\n'    
 'A MethodicalList subclasses a list and maps unimplemented attributes\n'    
 'into a callable which calls the corresponding method on each list members\n'    
 'and returns you a new `MethodicalList` with the results, ready for a\n'    
 'further dereference.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 '    >>> n = 1\n'    
 '    >>> class C(object):\n'    
 '    ...   def __init__(self):\n'    
 '    ...     global n\n'    
 '    ...     self.n = n\n'    
 '    ...     n += 1\n'    
 '    ...   def x(self):\n'    
 '    ...     return self.n\n'    
 '    ...\n'    
 '    >>> Cs=[ C(), C(), C() ]\n'    
 '    >>> ML = MethodicalList( Cs )\n'    
 '    >>> print(ML.x())\n'    
 '    [1, 2, 3]\n'    
 '\n'    
 '### Method `MethodicalList.__init__(self, initlist=None, strict=False)`\n'    
 '\n'    
 'Initialise the list.\n'    
 '\n'    
 'The optional parameter `initlist` initialises the list\n'    
 'as for a normal list.\n'    
 '\n'    
 'The optional parameter `strict`, if true, causes list elements\n'    
 'lacking the attribute to raise an AttributeError. If false,\n'    
 'list elements without the attribute are omitted from the results.\n'    
 '\n'    
 '## Function `named_column_tuples(rows, class_name=None, column_names=None, '    
 'computed=None, preprocess=None, mixin=None)`\n'    
 '\n'    
 'Process an iterable of data rows, usually with the first row being\n'    
 'column names.\n'    
 'Return a generated namedtuple factory and an iterable\n'    
 'of instances of the namedtuples for each row.\n'    
 '\n'    
 'Parameters:\n'    
 '* `rows`: an iterable of rows, each an iterable of data values.\n'    
 '* `class_name`: option class name for the namedtuple class\n'    
 '* `column_names`: optional iterable of column names used as the basis for\n'    
 '  the namedtuple. If this is not provided then the first row from\n'    
 '  `rows` is taken to be the column names.\n'    
 '* `computed`: optional mapping of str to functions of `self`\n'    
 '* `preprocess`: optional callable to modify CSV rows before\n'    
 '  they are converted into the namedtuple.  It receives a context\n'    
 '  object an the data row.\n'    
 '  It should return the row (possibly modified), or None to drop the\n'    
 '  row.\n'    
 '* `mixin`: an optional mixin class for the generated namedtuple subclass\n'    
 '  to provide extra methods or properties\n'    
 '\n'    
 'The context object passed to `preprocess` has the following attributes:\n'    
 '* `.cls`: attribute with the generated namedtuple subclass;\n'    
 '  this is useful for obtaining things like the column names\n'    
 '  or column indices;\n'    
 '  this is `None` when preprocessing the header row, if any\n'    
 "* `.index`: attribute with the row's enumeration, which counts from 0\n"    
 "* `.previous`: the previously accepted row's namedtuple,\n"    
 '  or `None` if there is no previous row\n'    
 '\n'    
 'Rows may be flat iterables in the same order as the column\n'    
 'names or mappings keyed on the column names.\n'    
 '\n'    
 'If the column names contain empty strings they are dropped\n'    
 'and the corresponding data row entries are also dropped. This\n'    
 'is very common with spreadsheet exports with unused padding\n'    
 'columns.\n'    
 '\n'    
 'Typical human readable column headings, also common in\n'    
 'speadsheet exports, are lowercased and have runs of whitespace\n'    
 'or punctuation turned into single underscores; trailing\n'    
 'underscores then get dropped.\n'    
 '\n'    
 'Basic example:\n'    
 '\n'    
 '    >>> data1 = [\n'    
 "    ...   ('a', 'b', 'c'),\n"    
 '    ...   (1, 11, "one"),\n'    
 '    ...   (2, 22, "two"),\n'    
 '    ... ]\n'    
 '    >>> cls, rows = named_column_tuples(data1)\n'    
 '    >>> print(list(rows))\n'    
 "    [NamedRow(a=1, b=11, c='one'), NamedRow(a=2, b=22, c='two')]\n"    
 '\n'    
 'Human readable column headings:\n'    
 '\n'    
 '    >>> data1 = [\n'    
 "    ...   ('Index', 'Value Found', 'Descriptive Text'),\n"    
 '    ...   (1, 11, "one"),\n'    
 '    ...   (2, 22, "two"),\n'    
 '    ... ]\n'    
 '    >>> cls, rows = named_column_tuples(data1)\n'    
 '    >>> print(list(rows))\n'    
 "    [NamedRow(index=1, value_found=11, descriptive_text='one'), "    
 "NamedRow(index=2, value_found=22, descriptive_text='two')]\n"    
 '\n'    
 'Rows which are mappings:\n'    
 '\n'    
 '    >>> data1 = [\n'    
 "    ...   ('a', 'b', 'c'),\n"    
 '    ...   (1, 11, "one"),\n'    
 '    ...   {\'a\': 2, \'c\': "two", \'b\': 22},\n'    
 '    ... ]\n'    
 '    >>> cls, rows = named_column_tuples(data1)\n'    
 '    >>> print(list(rows))\n'    
 "    [NamedRow(a=1, b=11, c='one'), NamedRow(a=2, b=22, c='two')]\n"    
 '\n'    
 'CSV export with unused padding columns:\n'    
 '\n'    
 '    >>> data1 = [\n'    
 "    ...   ('a', 'b', 'c', '', ''),\n"    
 '    ...   (1, 11, "one"),\n'    
 '    ...   {\'a\': 2, \'c\': "two", \'b\': 22},\n'    
 '    ...   [3, 11, "three", \'\', \'dropped\'],\n'    
 '    ... ]\n'    
 "    >>> cls, rows = named_column_tuples(data1, 'CSV_Row')\n"    
 '    >>> print(list(rows))\n'    
 "    [CSV_Row(a=1, b=11, c='one'), CSV_Row(a=2, b=22, c='two'), CSV_Row(a=3, "    
 "b=11, c='three')]\n"    
 '\n'    
 'A mixin class providing a `test1` method and a `test2` property:\n'    
 '\n'    
 '    >>> class Mixin(object):\n'    
 '    ...   def test1(self):\n'    
 '    ...     return "test1"\n'    
 '    ...   @property\n'    
 '    ...   def test2(self):\n'    
 '    ...     return "test2"\n'    
 '    >>> data1 = [\n'    
 "    ...   ('a', 'b', 'c'),\n"    
 '    ...   (1, 11, "one"),\n'    
 '    ...   {\'a\': 2, \'c\': "two", \'b\': 22},\n'    
 '    ... ]\n'    
 '    >>> cls, rows = named_column_tuples(data1, mixin=Mixin)\n'    
 '    >>> rows = list(rows)\n'    
 '    >>> rows[0].test1()\n'    
 "    'test1'\n"    
 '    >>> rows[0].test2\n'    
 "    'test2'\n"    
 '\n'    
 '## Function `named_row_tuple(*column_names, **kw)`\n'    
 '\n'    
 'Return a namedtuple subclass factory derived from `column_names`.\n'    
 '\n'    
 'Parameters:\n'    
 '* `column_names`: an iterable of `str`, such as the heading columns\n'    
 '  of a CSV export\n'    
 '* `class_name`: optional keyword parameter specifying the class name\n'    
 '* `computed`: optional keyword parameter providing a mapping\n'    
 '  of `str` to functions of `self`; these strings are available\n'    
 '  via `__getitem__`\n'    
 '* `mixin`: an optional mixin class for the generated namedtuple subclass\n'    
 '  to provide extra methods or properties\n'    
 '\n'    
 "The tuple's attributes are computed by converting all runs\n"    
 'of nonalphanumerics\n'    
 '(as defined by the `re` module\'s "\\W" sequence)\n'    
 'to an underscore, lowercasing and then stripping\n'    
 'leading and trailing underscores.\n'    
 '\n'    
 'In addition to the normal numeric indices, the tuple may\n'    
 'also be indexed by the attribute names or the column names.\n'    
 '\n'    
 'The new class has the following additional attributes:\n'    
 '* `attributes_`: the attribute names of each tuple in order\n'    
 '* `names_`: the originating name strings\n'    
 '* `name_attributes_`: the computed attribute names corresponding to the\n'    
 '  `names`; there may be empty strings in this list\n'    
 '* `attr_of_`: a mapping of column name to attribute name\n'    
 '* `name_of_`: a mapping of attribute name to column name\n'    
 '* `index_of_`: a mapping of column names and attributes their tuple indices\n'    
 '\n'    
 'Examples:\n'    
 '\n'    
 "    >>> T = named_row_tuple('Column 1', '', 'Column 3', ' Column 4', 'Column "    
 "5 ', '', '', class_name='Example')\n"    
 '    >>> T.attributes_\n'    
 "    ['column_1', 'column_3', 'column_4', 'column_5']\n"    
 "    >>> row = T('val1', 'dropped', 'val3', 4, 5, 6, 7)\n"    
 '    >>> row\n'    
 "    Example(column_1='val1', column_3='val3', column_4=4, column_5=5)\n"    
 '\n'    
 '## Class `PrefixedMappingProxy`\n'    
 '\n'    
 'A proxy for another mapping\n'    
 'operating on keys commencing with a prefix.\n'    
 '\n'    
 '## Class `SeenSet`\n'    
 '\n'    
 'A set-like collection with optional backing store file.\n'    
 '\n'    
 '### Method `SeenSet.add(self, s, foreign=False)`\n'    
 '\n'    
 'Add the value `s` to the set.\n'    
 '\n'    
 'Parameters:\n'    
 '* `s`: the value to add\n'    
 '* `foreign`: default `False`:\n'    
 '  whether the value came from an outside source,\n'    
 '  usually a third party addition to the backing file;\n'    
 '  this prevents appending the value to the backing file.\n'    
 '\n'    
 '## Class `SeqMapUC_Attrs`\n'    
 '\n'    
 'A wrapper for a mapping from keys\n'    
 '(matching the regular expression `^[A-Z][A-Z_0-9]*$`)\n'    
 'to tuples.\n'    
 '\n'    
 'Attributes matching such a key return the first element\n'    
 'of the sequence (and requires the sequence to have exactly on element).\n'    
 'An attribute `FOOs` or `FOOes`\n'    
 "(ending in a literal 's' or 'es', a plural)\n"    
 'returns the sequence (`FOO` must be a key of the mapping).\n'    
 '\n'    
 '## Class `StackableValues`\n'    
 '\n'    
 'A collection of named stackable values with the latest value\n'    
 'available as an attribute.\n'    
 '\n'    
 'I now recommand my `cs.context.stackattrs` context manager for most\n'    
 'uses; it may be applied to any object instead of requiring use of this\n'    
 'class.\n'    
 '\n'    
 'Note that names conflicting with methods are not available\n'    
 'as attributes and must be accessed via `__getitem__`.\n'    
 'As a matter of practice, in addition to the mapping methods,\n'    
 'avoid names which are verbs or which begin with an underscore.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 '    >>> S = StackableValues()\n'    
 '    >>> print(S)\n'    
 '    StackableValues()\n'    
 "    >>> S.push('x', 1)\n"    
 '    >>> print(S)\n'    
 '    StackableValues(x=1)\n'    
 '    >>> print(S.x)\n'    
 '    1\n'    
 "    >>> S.push('x', 2)\n"    
 '    1\n'    
 '    >>> print(S.x)\n'    
 '    2\n'    
 '    >>> S.x = 3\n'    
 '    >>> print(S.x)\n'    
 '    3\n'    
 "    >>> S.pop('x')\n"    
 '    3\n'    
 '    >>> print(S.x)\n'    
 '    1\n'    
 '    >>> with S.stack(x=4):\n'    
 '    ...   print(S.x)\n'    
 '    ...\n'    
 '    4\n'    
 '    >>> print(S.x)\n'    
 '    1\n'    
 '    >>> S.update(x=5)\n'    
 "    {'x': 1}\n"    
 '\n'    
 '### Method `StackableValues.__getattr__(self, attr)`\n'    
 '\n'    
 'Convenience: present the top value of key `attr` as an attribute.\n'    
 '\n'    
 'Note that attributes `push`, `pop` and the mapping method names\n'    
 'are shadowed by the instance methods\n'    
 'and should be accessed with the traditional `[]` key dereference.\n'    
 '\n'    
 '### Method `StackableValues.__getitem__(self, key)`\n'    
 '\n'    
 'Return the top value for `key` or raise `KeyError`.\n'    
 '\n'    
 '### Method `StackableValues.__setattr__(self, attr, value)`\n'    
 '\n'    
 'For nonunderscore attributes, replace the top element of the stack.\n'    
 '\n'    
 '### Method `StackableValues.get(self, key, default=None)`\n'    
 '\n'    
 'Get the top value for `key`, or `default`.\n'    
 '\n'    
 '### Method `StackableValues.items(self)`\n'    
 '\n'    
 'Mapping method returning an iterable of (name, value) tuples.\n'    
 '\n'    
 '### Method `StackableValues.keys(self)`\n'    
 '\n'    
 'Mapping method returning a list of the names.\n'    
 '\n'    
 '### Method `StackableValues.pop(self, key)`\n'    
 '\n'    
 'Pop and return the latest value for `key`.\n'    
 '\n'    
 '### Method `StackableValues.push(self, key, value)`\n'    
 '\n'    
 'Push a new `value` for `key`.\n'    
 'Return the previous value\n'    
 'or `None` if this is the first value for `key`.\n'    
 '\n'    
 '### Method `StackableValues.stack(self, *a, **kw)`\n'    
 '\n'    
 'Context manager which saves and restores the current state.\n'    
 'Any parameters are passed to `update()` after the save\n'    
 'but before the yield.\n'    
 '\n'    
 '### Method `StackableValues.update(self, *ms, **kw)`\n'    
 '\n'    
 'Update the mapping like `dict.update` method.\n'    
 'Return a mapping with the preupdate values\n'    
 'of the updated keys.\n'    
 '\n'    
 '### Method `StackableValues.values(self)`\n'    
 '\n'    
 'Mapping method returning an iterable of the values.\n'    
 '\n'    
 '## Class `UC_Sequence(builtins.list)`\n'    
 '\n'    
 'A tuple-of-nodes on which `.ATTRs` indirection can be done,\n'    
 'yielding another tuple-of-nodes or tuple-of-values.\n'    
 '\n'    
 '### Method `UC_Sequence.__init__(self, Ns)`\n'    
 '\n'    
 'Initialise from an iterable sequence.\n'    
 '\n'    
 '## Class '    
 '`UUIDedDict(builtins.dict,JSONableMappingMixin,AttrableMappingMixin)`\n'    
 '\n'    
 'A handy `dict` subtype providing the basis for mapping classes\n'    
 'indexed by `UUID`s.\n'    
 '\n'    
 "The `'uuid'` attribute is always a `UUID` instance.\n"    
 '\n'    
 '### Method `UUIDedDict.__init__(self, _d=None, **kw)`\n'    
 '\n'    
 'Initialise the `UUIDedDict`,\n'    
 "generating a `'uuid'` key value if omitted.\n"    
 '\n'    
 '### Property `UUIDedDict.uuid`\n'    
 '\n'    
 "A UUID from `self['uuid']`.\n"    
 '\n'    
 'This does a sanity check that the stored value is a `UUID`,\n'    
 'but primarily exists to support the setter,\n'    
 'which promotes `str` to `UUID`, thus also validating UUID strings.\n'    
 '\n'    
 '# Release Log\n'    
 '\n'    
 '\n'    
 '\n'    
 '*Release 20201228*:\n'    
 'New PrefixedMappingProxy presenting the keys of another mapping commencing '    
 'with a prefix.\n'    
 '\n'    
 '*Release 20201102*:\n'    
 '* StackableValues is obsolete, add recommendation for cs.context.stackattrs '    
 'to the docstring.\n'    
 '* New AttrableMappingMixin with a __getattr__ which looks up unknown '    
 'attributes as keys.\n'    
 '* New JSONableMappingMixin with methods for JSON actions: from_json, '    
 'as_json, append_ndjson and a __str__ and __repr__.\n'    
 '* New LoadableMappingMixin to load .by_* attributes on demand.\n'    
 '* New AttrableMapping(dict, AttrableMappingMixin).\n'    
 '\n'    
 '*Release 20200130*:\n'    
 'New dicts_to_namedtuples function to yield namedtuples from an iterable of '    
 'dicts.\n'    
 '\n'    
 '*Release 20191120*:\n'    
 'named_row_tuple: support None in a column name, as from Excel unfilled '    
 'heading row entries\n'    
 '\n'    
 '*Release 20190617*:\n'    
 '* StackableValues.push now returns the previous value.\n'    
 '* StackableValues.update has a signature like dict.update.\n'    
 '* StackableValues.pop removes entries when their stack becomes empty.\n'    
 '* StackableValues.stack: clean implementation of save/restore.\n'    
 '* StackableValues: avoid infinite recursion through ._fallback.\n'    
 '* StackableValues.keys now returns a list of the nonempty keys.\n'    
 '* Update doctests.\n'    
 '\n'    
 '*Release 20190103*:\n'    
 'Documentation update.\n'    
 '\n'    
 '*Release 20181231*:\n'    
 '* Bugfix for mapping of column names to row indices.\n'    
 '* New subclass._fallback method for when a stack is empty.\n'    
 '\n'    
 '*Release 20180720*:\n'    
 'Initial PyPI release specificly for named_column_tuple and '    
 'named_column_tuples.'),
  classifiers = ['Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)'],
  install_requires = ['cs.deco', 'cs.lex', 'cs.logutils', 'cs.pfx', 'cs.py3', 'cs.seq', 'cs.sharedfile'],
  keywords = ['python2', 'python3'],
  license = 'GNU General Public License v3 or later (GPLv3+)',
  long_description_content_type = 'text/markdown',
  package_dir = {'': 'lib/python'},
  py_modules = ['cs.mappings'],
)
