#!/usr/bin/env python
from setuptools import setup
setup(
  name = 'cs.sqltags',
  author = 'Cameron Simpson',
  author_email = 'cs@cskk.id.au',
  version = '20211212',
  url = 'https://bitbucket.org/cameron_simpson/css/commits/all',
  description =
    ('Simple SQL based tagging and the associated `sqltags` command line script, '    
 'supporting both tagged named objects and tagged timestamped log entries.'),
  long_description =
    ('Simple SQL based tagging\n'    
 'and the associated `sqltags` command line script,\n'    
 'supporting both tagged named objects and tagged timestamped log entries.\n'    
 '\n'    
 '*Latest release 20211212*:\n'    
 '* Rename edit_many to edit_tagsets for clarity.\n'    
 '* Small bugfixes.\n'    
 '\n'    
 'Compared to `cs.fstags` and its associated `fstags` command,\n'    
 'this is oriented towards large numbers of items\n'    
 'not naturally associated with filesystem objects.\n'    
 '\n'    
 'My initial use case is an activity log\n'    
 '(unnamed timestamped tag sets)\n'    
 "but I'm also using it for ontologies\n"    
 '(named tag sets containing metadata).\n'    
 '\n'    
 'Many basic tasks can be performed with the `sqltags` command line utility,\n'    
 'documented under the `SQLTagsCommand` class below.\n'    
 '\n'    
 'See the `SQLTagsORM` documentation for details about how data\n'    
 'are stored in the database.\n'    
 'See the `SQLTagSet` documentation for details of how various\n'    
 'tag value types are supported.\n'    
 '\n'    
 '## Class '    
 '`BaseSQLTagsCommand(cs.cmdutils.BaseCommand,cs.tagset.TagsCommandMixin)`\n'    
 '\n'    
 'Common features for commands oriented around an `SQLTags` database.\n'    
 '\n'    
 'Command line usage:\n'    
 '\n'    
 '    Usage: basesqltags [-f db_url] subcommand [...]\n'    
 '      -f db_url SQLAlchemy database URL or filename.\n'    
 '                Default from $SQLTAGS_DBURL (default '    
 "'~/var/sqltags.sqlite').\n"    
 '      Subcommands:\n'    
 '        dbshell\n'    
 '          Start an interactive database shell.\n'    
 '        edit criteria...\n'    
 '          Edit the entities specified by criteria.\n'    
 '        export [-F format] [{tag[=value]|-tag}...]\n'    
 '          Export entities matching all the constraints.\n'    
 '          -F format Specify the export format, either CSV or FSTAGS.\n'    
 '        find [-o output_format] {tag[=value]|-tag}...\n'    
 '          List entities matching all the constraints.\n'    
 '          -o output_format\n'    
 '                      Use output_format as a Python format string to lay '    
 'out\n'    
 '                      the listing.\n'    
 '                      Default: {datetime} {headline}\n'    
 '        help [subcommand-names...]\n'    
 '          Print the full help for the named subcommands,\n'    
 '          or for all subcommands if no names are specified.\n'    
 '        import [{-u|--update}] {-|srcpath}...\n'    
 '          Import CSV data in the format emitted by "export".\n'    
 '          Each argument is a file path or "-", indicating standard input.\n'    
 '          -u, --update  If a named entity already exists then update its '    
 'tags.\n'    
 '                        Otherwise this will be seen as a conflict\n'    
 '                        and the import aborted.\n'    
 '        init\n'    
 '          Initialise the database.\n'    
 '          This includes defining the schema and making the root metanode.\n'    
 '        log [-c category,...] [-d when] [-D strptime] {-|headline} '    
 '[tags...]\n'    
 '          Record entries into the database.\n'    
 "          If headline is '-', read headlines from standard input.\n"    
 '          -c categories\n'    
 '            Specify the categories for this log entry.\n'    
 '            The default is to recognise a leading CAT,CAT,...: prefix.\n'    
 '          -d when\n'    
 '            Use when, an ISO8601 date, as the log entry timestamp.\n'    
 '          -D strptime\n'    
 '            Read the time from the start of the headline\n'    
 '            according to the provided strptime specification.\n'    
 '        tag {-|entity-name} {tag[=value]|-tag}...\n'    
 '          Tag an entity with multiple tags.\n'    
 '          With the form "-tag", remove that tag from the direct tags.\n'    
 '          A entity-name named "-" indicates that entity-names should\n'    
 '          be read from the standard input.\n'    
 '\n'    
 '### `BaseSQLTagsCommand.TAGSETS_CLASS`\n'    
 '\n'    
 '### `BaseSQLTagsCommand.TAGSET_CRITERION_CLASS`\n'    
 '\n'    
 '### `BaseSQLTagsCommand.TAG_BASED_TEST_CLASS`\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.apply_defaults(self)`\n'    
 '\n'    
 'Set up the default values in `options`.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.apply_opt(self, opt, val)`\n'    
 '\n'    
 'Apply a command line option.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_dbshell(self, argv)`\n'    
 '\n'    
 'Usage: {cmd}\n'    
 'Start an interactive database shell.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_edit(self, argv)`\n'    
 '\n'    
 'Usage: edit criteria...\n'    
 'Edit the entities specified by criteria.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_export(self, argv)`\n'    
 '\n'    
 'Usage: {cmd} [-F format] [{{tag[=value]|-tag}}...]\n'    
 'Export entities matching all the constraints.\n'    
 '-F format Specify the export format, either CSV or FSTAGS.\n'    
 '\n'    
 'The CSV export format is CSV data with the following columns:\n'    
 '* unixtime: the entity unixtime, a float\n'    
 '* id: the entity database row id, an integer\n'    
 '* name: the entity name\n'    
 '* tags: a column per Tag\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_find(self, argv)`\n'    
 '\n'    
 'Usage: {cmd} [-o output_format] {{tag[=value]|-tag}}...\n'    
 'List entities matching all the constraints.\n'    
 '-o output_format\n'    
 '            Use output_format as a Python format string to lay out\n'    
 '            the listing.\n'    
 '            Default: {FIND_OUTPUT_FORMAT_DEFAULT}\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_import(self, argv)`\n'    
 '\n'    
 'Usage: {cmd} [{{-u|--update}}] {{-|srcpath}}...\n'    
 '  Import CSV data in the format emitted by "export".\n'    
 '  Each argument is a file path or "-", indicating standard input.\n'    
 '  -u, --update  If a named entity already exists then update its tags.\n'    
 '                Otherwise this will be seen as a conflict\n'    
 '                and the import aborted.\n'    
 '\n'    
 'TODO: should this be a transaction so that an import is all or nothing?\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_init(self, argv)`\n'    
 '\n'    
 'Usage: {cmd}\n'    
 'Initialise the database.\n'    
 'This includes defining the schema and making the root metanode.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_log(self, argv)`\n'    
 '\n'    
 'Record a log entry.\n'    
 '\n'    
 'Usage: {cmd} [-c category,...] [-d when] [-D strptime] {{-|headline}} '    
 '[tags...]\n'    
 '  Record entries into the database.\n'    
 "  If headline is '-', read headlines from standard input.\n"    
 '  -c categories\n'    
 '    Specify the categories for this log entry.\n'    
 '    The default is to recognise a leading CAT,CAT,...: prefix.\n'    
 '  -d when\n'    
 '    Use when, an ISO8601 date, as the log entry timestamp.\n'    
 '  -D strptime\n'    
 '    Read the time from the start of the headline\n'    
 '    according to the provided strptime specification.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.cmd_tag(self, argv)`\n'    
 '\n'    
 'Usage: {cmd} {{-|entity-name}} {{tag[=value]|-tag}}...\n'    
 'Tag an entity with multiple tags.\n'    
 'With the form "-tag", remove that tag from the direct tags.\n'    
 'A entity-name named "-" indicates that entity-names should\n'    
 'be read from the standard input.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.parse_categories(categories)`\n'    
 '\n'    
 'Extract "category" words from the `str` `categories`,\n'    
 'return a list of category names.\n'    
 '\n'    
 'Splits on commas, strips leading and trailing whitespace, downcases.\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.parse_tagset_criterion(arg, '    
 'tag_based_test_class=None)`\n'    
 '\n'    
 'Parse tag criteria from `argv`.\n'    
 '\n'    
 'The criteria may be either:\n'    
 '* an integer specifying a `Tag` id\n'    
 '* a sequence of tag criteria\n'    
 '\n'    
 '### Method `BaseSQLTagsCommand.run_context(self)`\n'    
 '\n'    
 'Prepare the `SQLTags` around each command invocation.\n'    
 '\n'    
 '## Function `glob2like(glob: str) -> str`\n'    
 '\n'    
 'Convert a filename glob to an SQL LIKE pattern.\n'    
 '\n'    
 '## Function `main(argv=None)`\n'    
 '\n'    
 'Command line mode.\n'    
 '\n'    
 '## Class `PolyValue(PolyValue,builtins.tuple)`\n'    
 '\n'    
 'A `namedtuple` for the polyvalues used in an `SQLTagsORM`.\n'    
 '\n'    
 'We express various types in SQL as one of 3 columns:\n'    
 '* `float_value`: for `float`s and `int`s which round trip with `float`\n'    
 '* `string_value`: for `str`\n'    
 '* `structured_value`: a JSON transcription of any other type\n'    
 '\n'    
 'This allows SQL indexing of basic types.\n'    
 '\n'    
 'Note that because `str` gets stored in `string_value`\n'    
 'this leaves us free to use "bare string" JSON to serialise\n'    
 'various nonJSONable types.\n'    
 '\n'    
 'The `SQLTagSets` class has a `to_polyvalue` factory\n'    
 'which produces a `PolyValue` suitable for the SQL rows.\n'    
 'NonJSONable types such as `datetime`\n'    
 'are converted to a `str` but stored in the `structured_value` column.\n'    
 'This should be overridden by subclasses as necessary.\n'    
 '\n'    
 'On retrieval from the database\n'    
 'the tag rows are converted to Python values\n'    
 'by the `SQLTagSets.from_polyvalue` method,\n'    
 'reversing the process above.\n'    
 '\n'    
 '### Method `PolyValue.is_valid(self)`\n'    
 '\n'    
 'Test that at most one attribute is non-`None`.\n'    
 '\n'    
 '## Class `PolyValueColumnMixin`\n'    
 '\n'    
 'A mixin for classes with `(float_value,string_value,structured_value)` '    
 'columns.\n'    
 'This is used by the `Tags` and `TagMultiValues` relations inside '    
 '`SQLTagsORM`.\n'    
 '\n'    
 '### Method `PolyValueColumnMixin.as_polyvalue(self)`\n'    
 '\n'    
 "Return this row's value as a `PolyValue`.\n"    
 '\n'    
 '### Method `PolyValueColumnMixin.set_polyvalue(self, pv: '    
 'cs.sqltags.PolyValue)`\n'    
 '\n'    
 'Set all the value fields.\n'    
 '\n'    
 '### Method `PolyValueColumnMixin.value_test(other_value)`\n'    
 '\n'    
 'Return `(column,test_value)` for constructing tests against\n'    
 '`other_value` where `column` if the appropriate SQLAlchemy column\n'    
 'and `test_value` is the comparison value for testing.\n'    
 '\n'    
 'For most `other_value`s the `test_value`\n'    
 'will just be `other_value`,\n'    
 'but for certain types the `test_value` will be:\n'    
 '* `NoneType`: `None`, and the column will also be `None`\n'    
 '* `datetime`: `datetime2unixtime(other_value)`\n'    
 '\n'    
 "## Function `prefix2like(prefix: str, esc='\\\\') -> str`\n"    
 '\n'    
 'Convert a prefix string to an SQL LIKE pattern.\n'    
 '\n'    
 '## Class `SQLParameters(SQLParameters,builtins.tuple)`\n'    
 '\n'    
 'The parameters required for constructing queries\n'    
 'or extending queries with JOINs.\n'    
 '\n'    
 'Attributes:\n'    
 '* `criterion`: the source criterion, usually an `SQTCriterion` subinstance\n'    
 '* `alias`: an alias of the source table for use in queries\n'    
 '* `entity_id_column`: the `entities` id column,\n'    
 '  `alias.id` if the alias is of `entities`,\n'    
 '  `alias.entity_id` if the alias is of `tags`\n'    
 '* `constraint`: a filter query based on `alias`\n'    
 '\n'    
 '## Class '    
 '`SQLTagBasedTest(cs.tagset.TagBasedTest,cs.tagset.TagBasedTest,builtins.tuple,SQTCriterion,cs.tagset.TagSetCriterion)`\n'    
 '\n'    
 'A `cs.tagset.TagBasedTest` extended with a `.sql_parameters` method.\n'    
 '\n'    
 '### Method `SQLTagBasedTest.match_tagged_entity(self, te: cs.tagset.TagSet) '    
 '-> bool`\n'    
 '\n'    
 'Match this criterion against `te`.\n'    
 '\n'    
 '## Class `SQLTagProxies`\n'    
 '\n'    
 'A proxy for the tags supporting Python comparison => `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    sqltags.tags.dotted.name.here == 'foo'\n"    
 '\n'    
 '## Class `SQLTagProxy`\n'    
 '\n'    
 'An object based on a `Tag` name\n'    
 'which produces an `SQLParameters` when compared with some value.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqltags = SQLTags('sqlite://')\n"    
 '    >>> sqltags.init()\n'    
 "    >>> # make a SQLParameters for testing the tag 'name.thing'==5\n"    
 '    >>> sqlp = sqltags.tags.name.thing == 5\n'    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.float_value = :float_value_1'\n"    
 "    >>> sqlp = sqltags.tags.name.thing == 'foo'\n"    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.string_value = :string_value_1'\n"    
 '\n'    
 '### Method `SQLTagProxy.__eq__(self, other, alias=None) -> '    
 'cs.sqltags.SQLParameters`\n'    
 '\n'    
 'Return an SQL `=` test `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing == 'foo'\n"    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.string_value = :string_value_1'\n"    
 '\n'    
 '### Method `SQLTagProxy.__ge__(self, other)`\n'    
 '\n'    
 'Return an SQL `>=` test `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing >= 'foo'\n"    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.string_value >= :string_value_1'\n"    
 '\n'    
 '### Method `SQLTagProxy.__getattr__(self, sub_tag_name)`\n'    
 '\n'    
 'Magic access to dotted tag names: produce a new `SQLTagProxy` from ourself.\n'    
 '\n'    
 '### Method `SQLTagProxy.__gt__(self, other)`\n'    
 '\n'    
 'Return an SQL `>` test `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing > 'foo'\n"    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.string_value > :string_value_1'\n"    
 '\n'    
 '### Method `SQLTagProxy.__le__(self, other)`\n'    
 '\n'    
 'Return an SQL `<=` test `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing <= 'foo'\n"    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.string_value <= :string_value_1'\n"    
 '\n'    
 '### Method `SQLTagProxy.__lt__(self, other)`\n'    
 '\n'    
 'Return an SQL `<` test `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing < 'foo'\n"    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.string_value < :string_value_1'\n"    
 '\n'    
 '### Method `SQLTagProxy.__ne__(self, other, alias=None) -> '    
 'cs.sqltags.SQLParameters`\n'    
 '\n'    
 'Return an SQL `<>` test `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing != 'foo'\n"    
 '    >>> str(sqlp.constraint)\n'    
 "    'tags_1.name = :name_1 AND tags_1.string_value != :string_value_1'\n"    
 '\n'    
 '### Method `SQLTagProxy.by_op_text(self, op_text, other, alias=None)`\n'    
 '\n'    
 "Return an `SQLParameters` based on the comparison's text representation.\n"    
 '\n'    
 'Parameters:\n'    
 '* `op_text`: the comparsion operation text, one of:\n'    
 "  `'='`, `'<='`, `'<'`, `'>='`, `'>'`, `'~'`.\n"    
 '* `other`: the other value for the comparison,\n'    
 '  used to infer the SQL column name\n'    
 '  and kept to provide the SQL value parameter\n'    
 '* `alias`: optional SQLAlchemy table alias\n'    
 '\n'    
 '### Method `SQLTagProxy.likeglob(self, globptn: str) -> '    
 'cs.sqltags.SQLParameters`\n'    
 '\n'    
 'Return an SQL LIKE test approximating a glob as an `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing.likeglob('foo*')\n"    
 '    >>> str(sqlp.constraint)\n'    
 '    "tags_1.name = :name_1 AND tags_1.string_value LIKE :string_value_1 '    
 'ESCAPE \'\\\\\'"\n'    
 '\n'    
 '### Method `SQLTagProxy.startswith(self, prefix: str) -> '    
 'cs.sqltags.SQLParameters`\n'    
 '\n'    
 'Return an SQL LIKE prefix test `SQLParameters`.\n'    
 '\n'    
 'Example:\n'    
 '\n'    
 "    >>> sqlp = SQLTags('sqlite://').tags.name.thing.startswith('foo')\n"    
 '    >>> str(sqlp.constraint)\n'    
 '    "tags_1.name = :name_1 AND tags_1.string_value LIKE :string_value_1 '    
 'ESCAPE \'\\\\\'"\n'    
 '\n'    
 '## Class '    
 '`SQLTags(cs.tagset.BaseTagSets,cs.resources.MultiOpenMixin,cs.context.ContextManagerMixin,collections.abc.MutableMapping,collections.abc.Mapping,collections.abc.Collection,collections.abc.Sized,collections.abc.Iterable,collections.abc.Container)`\n'    
 '\n'    
 'A class using an SQL database to store its `TagSet`s.\n'    
 '\n'    
 '### Method `SQLTags.TAGSETCLASS_DEFAULT(self, *a, _sqltags=None, **kw)`\n'    
 '\n'    
 'Factory to return a suitable `TagSet` subclass instance.\n'    
 'This produces an `SQLTagSet` instance correctly associated with this '    
 '`SQLTags`.\n'    
 '\n'    
 '### Method `SQLTags.TagSetClass(self, *a, **kw)`\n'    
 '\n'    
 'Local implementation of `TagSetClass`\n'    
 'so that we can annotate it with a `.singleton_also_by` attribute.\n'    
 '\n'    
 '### Method `SQLTags.__getitem__(self, *a, **kw)`\n'    
 '\n'    
 'Return an `SQLTagSet` for `index` (an `int` or `str`).\n'    
 '\n'    
 '### Method `SQLTags.__setitem__(self, *a, **kw)`\n'    
 '\n'    
 'Dummy `__setitem__` which checks `te` against the db by type\n'    
 'because the factory inserts it into the database.\n'    
 '\n'    
 '### Method `SQLTags.db_entity(self, index)`\n'    
 '\n'    
 'Return the `Entities` instance for `index` or `None`.\n'    
 '\n'    
 '### Method `SQLTags.db_session(self, *, new=False)`\n'    
 '\n'    
 'Context manager to obtain a db session if required,\n'    
 'just a shim for `self.orm.session()`.\n'    
 '\n'    
 '### Property `SQLTags.default_db_session`\n'    
 '\n'    
 'The current per-`Thread` SQLAlchemy Session.\n'    
 '\n'    
 "### Method `SQLTags.default_factory(self, name: [<class 'str'>, None], *, "    
 'unixtime=None, tags=None)`\n'    
 '\n'    
 'Fetch or create an `SQLTagSet` for `name`.\n'    
 '\n'    
 'Note that `name` may be `None` to create a new "log" entry.\n'    
 '\n'    
 '### Method `SQLTags.find(self, criteria)`\n'    
 '\n'    
 'Generate and run a query derived from `criteria`\n'    
 'yielding `SQLTagSet` instances.\n'    
 '\n'    
 'Parameters:\n'    
 '* `criteria`: an iterable of search criteria\n'    
 '  which should be `SQTCriterion`s\n'    
 '  or a `str` suitable for `SQTCriterion.from_str`.\n'    
 '\n'    
 '### Method `SQLTags.flush(self)`\n'    
 '\n'    
 'Flush the current session state to the database.\n'    
 '\n'    
 '### Method `SQLTags.get(self, index, default=None)`\n'    
 '\n'    
 'Return an `SQLTagSet` matching `index`, or `None` if there is no such '    
 'entity.\n'    
 '\n'    
 '### Method `SQLTags.import_csv_file(self, f, *, update_mode=False)`\n'    
 '\n'    
 'Import CSV data from the file `f`.\n'    
 '\n'    
 'If `update_mode` is true\n'    
 'named records which already exist will update from the data,\n'    
 'otherwise the conflict will raise a `ValueError`.\n'    
 '\n'    
 '### Method `SQLTags.import_tagged_entity(self, te, *, update_mode=False) -> '    
 'None`\n'    
 '\n'    
 'Import the `TagSet` `te`.\n'    
 '\n'    
 'This updates the database with the contents of the supplied `TagSet`,\n'    
 'which has no inherent relationship to the database.\n'    
 '\n'    
 'If `update_mode` is true\n'    
 'named records which already exist will update from `te`,\n'    
 'otherwise the conflict will raise a `ValueError`.\n'    
 '\n'    
 '### Method `SQLTags.infer_db_url(envvar=None, default_path=None)`\n'    
 '\n'    
 'Infer the database URL.\n'    
 '\n'    
 'Parameters:\n'    
 '* `envvar`: environment variable to specify a default,\n'    
 '  default from `DBURL_ENVVAR` (`SQLTAGS_DBURL`).\n'    
 '\n'    
 '### Method `SQLTags.init(self)`\n'    
 '\n'    
 'Initialise the database.\n'    
 '\n'    
 '### Method `SQLTags.items(self, *, prefix=None)`\n'    
 '\n'    
 'Return an iterable of `(tagset_name,TagSet)`.\n'    
 'Excludes unnamed `TagSet`s.\n'    
 '\n'    
 'Constrain the names to those starting with `prefix`\n'    
 'if not `None`.\n'    
 '\n'    
 '### Method `SQLTags.keys(self, *, prefix=None)`\n'    
 '\n'    
 'Yield all the nonNULL names.\n'    
 '\n'    
 'Constrain the names to those starting with `prefix`\n'    
 'if not `None`.\n'    
 '\n'    
 '### Property `SQLTags.metanode`\n'    
 '\n'    
 'The metadata node.\n'    
 '\n'    
 '### Method `SQLTags.startup_shutdown(self)`\n'    
 '\n'    
 'Stub startup/shutdown since we use autosessions.\n'    
 'Particularly, we do not want to keep SQLite dbs open.\n'    
 '\n'    
 '### Method `SQLTags.values(self, *, prefix=None)`\n'    
 '\n'    
 'Return an iterable of the named `TagSet`s.\n'    
 'Excludes unnamed `TagSet`s.\n'    
 '\n'    
 'Constrain the names to those starting with `prefix`\n'    
 'if not `None`.\n'    
 '\n'    
 '## Class '    
 '`SQLTagsCommand(BaseSQLTagsCommand,cs.cmdutils.BaseCommand,cs.tagset.TagsCommandMixin)`\n'    
 '\n'    
 '`sqltags` main command line utility.\n'    
 '\n'    
 'Command line usage:\n'    
 '\n'    
 '    Usage: sqltags [-f db_url] subcommand [...]\n'    
 '      -f db_url SQLAlchemy database URL or filename.\n'    
 '                Default from $SQLTAGS_DBURL (default '    
 "'~/var/sqltags.sqlite').\n"    
 '      Subcommands:\n'    
 '        dbshell\n'    
 '          Start an interactive database shell.\n'    
 '        edit criteria...\n'    
 '          Edit the entities specified by criteria.\n'    
 '        export [-F format] [{tag[=value]|-tag}...]\n'    
 '          Export entities matching all the constraints.\n'    
 '          -F format Specify the export format, either CSV or FSTAGS.\n'    
 '        find [-o output_format] {tag[=value]|-tag}...\n'    
 '          List entities matching all the constraints.\n'    
 '          -o output_format\n'    
 '                      Use output_format as a Python format string to lay '    
 'out\n'    
 '                      the listing.\n'    
 '                      Default: {datetime} {headline}\n'    
 '        help [subcommand-names...]\n'    
 '          Print the full help for the named subcommands,\n'    
 '          or for all subcommands if no names are specified.\n'    
 '        import [{-u|--update}] {-|srcpath}...\n'    
 '          Import CSV data in the format emitted by "export".\n'    
 '          Each argument is a file path or "-", indicating standard input.\n'    
 '          -u, --update  If a named entity already exists then update its '    
 'tags.\n'    
 '                        Otherwise this will be seen as a conflict\n'    
 '                        and the import aborted.\n'    
 '        init\n'    
 '          Initialise the database.\n'    
 '          This includes defining the schema and making the root metanode.\n'    
 '        list [entity-names...]\n'    
 '          List entities and their tags.\n'    
 '        log [-c category,...] [-d when] [-D strptime] {-|headline} '    
 '[tags...]\n'    
 '          Record entries into the database.\n'    
 "          If headline is '-', read headlines from standard input.\n"    
 '          -c categories\n'    
 '            Specify the categories for this log entry.\n'    
 '            The default is to recognise a leading CAT,CAT,...: prefix.\n'    
 '          -d when\n'    
 '            Use when, an ISO8601 date, as the log entry timestamp.\n'    
 '          -D strptime\n'    
 '            Read the time from the start of the headline\n'    
 '            according to the provided strptime specification.\n'    
 '        ls [entity-names...]\n'    
 '          List entities and their tags.\n'    
 '        tag {-|entity-name} {tag[=value]|-tag}...\n'    
 '          Tag an entity with multiple tags.\n'    
 '          With the form "-tag", remove that tag from the direct tags.\n'    
 '          A entity-name named "-" indicates that entity-names should\n'    
 '          be read from the standard input.\n'    
 '\n'    
 '### Method `SQLTagsCommand.cmd_list(self, argv)`\n'    
 '\n'    
 'Usage: {cmd} [entity-names...]\n'    
 'List entities and their tags.\n'    
 '\n'    
 '### Method `SQLTagsCommand.cmd_ls(self, argv)`\n'    
 '\n'    
 'Usage: {cmd} [entity-names...]\n'    
 'List entities and their tags.\n'    
 '\n'    
 '## Class '    
 '`SQLTagSet(cs.obj.SingletonMixin,cs.tagset.TagSet,builtins.dict,cs.dateutils.UNIXTimeMixin,cs.lex.FormatableMixin,cs.lex.FormatableFormatter,string.Formatter,cs.mappings.AttrableMappingMixin)`\n'    
 '\n'    
 'A singleton `TagSet` attached to an `SQLTags` instance.\n'    
 '\n'    
 'As with the `TagSet` superclass,\n'    
 'tag values can be any Python type.\n'    
 'However, because we are storing these values in an SQL database\n'    
 'it is necessary to provide a conversion facility\n'    
 'to prepare those values for storage.\n'    
 '\n'    
 'The database schema is described in the `SQLTagsORM` class;\n'    
 'in short we directly support `None`, `float` and `str`,\n'    
 '`int`s which round trip with `float`,\n'    
 'and `list`, `tuple` and `dict` whose contents transcribe to JSON.\n'    
 '\n'    
 '`int`s which are too large to round trip with `float`\n'    
 'are treated as an extended `"bigint"` type\n'    
 'using the scheme described below.\n'    
 '\n'    
 'Because the ORM has distinct `float` and `str` columns to support indexing,\n'    
 'there will be no plain strings in the remaining JSON blob column.\n'    
 'Therefore we support other types by providing functions\n'    
 'to convert each type to a `str` and back,\n'    
 'and an associated "type label" which will be prefixed to the string;\n'    
 'the resulting string is stored in the JSON blob.\n'    
 '\n'    
 'The default mechanism is based on the following class attributes and '    
 'methods:\n'    
 '* `TYPE_JS_MAPPING`: a mapping of a type label string\n'    
 '  to a 3 tuple of `(type,to_str,from_str)`\n'    
 '  being the extended type,\n'    
 '  a function to convert an instance to `str`\n'    
 '  and a function to convert a `str` to an instance of this type\n'    
 '* `to_js_str`: a method accepting `(tag_name,tag_value)`\n'    
 '  and returning `tag_value` as a `str`;\n'    
 '  the default implementation looks up the type of `tag_value`\n'    
 '  in `TYPE_JS_MAPPING` to locate the corresponding `to_str` function\n'    
 '* `from_js_str`: a method accepting `(tag_name,js)`\n'    
 '  which uses the leading type label prefix from the `js`\n'    
 '  to look up the corresponding `from_str` function\n'    
 '  from `TYPE_JS_MAPPING` and use it on the tail of `js`\n'    
 '\n'    
 'The default `TYPE_JS_MAPPING` has mappings for:\n'    
 '* `"bigint"`: conversions for `int`\n'    
 '* `"date"`: conversions for `datetime.date`\n'    
 '* `"datetime"`: conversions for `datetime.datetime`\n'    
 '\n'    
 'Subclasses wanting to augument the `TYPE_JS_MAPPING`\n'    
 'should prepare their own with code such as:\n'    
 '\n'    
 '    class SubSQLTagSet(SQLTagSet,....):\n'    
 '        ....\n'    
 '        TYPE_JS_MAPPING=dict(SQLTagSet.TYPE_JS_MAPPING)\n'    
 '        TYPE_JS_MAPPING.update(\n'    
 '          typelabel=(type, to_str, from_str),\n'    
 '          ....\n'    
 '        )\n'    
 '\n'    
 '### Method `SQLTagSet.add_db_tag(self, *a, **kw)`\n'    
 '\n'    
 'Add a tag to the database.\n'    
 '\n'    
 "### Method `SQLTagSet.child_tagsets(self, tag_name='parent')`\n"    
 '\n'    
 'Return the child `TagSet`s as defined by their parent `Tag`,\n'    
 "by default the `Tag` named `'parent'`.\n"    
 '\n'    
 '### Method `SQLTagSet.db_session(self, new=False)`\n'    
 '\n'    
 'Context manager to obtain a new session if required,\n'    
 'just a shim for `self.sqltags.db_session`.\n'    
 '\n'    
 '### Method `SQLTagSet.discard_db_tag(self, tag_name: str, pv: '    
 'Optional[cs.sqltags.PolyValue] = None)`\n'    
 '\n'    
 'Discard a tag from the database.\n'    
 '\n'    
 '### Method `SQLTagSet.from_js_str(tag_name: str, js: str)`\n'    
 '\n'    
 'Convert the `str` `js` to a `Tag` value.\n'    
 'This is the reverse of `as_js_str`.\n'    
 '\n'    
 'Subclasses wanting extra type support\n'    
 'should either:\n'    
 '(usual approach) provide their own `TYPE_JS_MAPPING` class attribute\n'    
 'as described at the top of this class\n'    
 'or (for unusual requirements) override this method and also `to_js_str`.\n'    
 '\n'    
 '### Method `SQLTagSet.from_polyvalue(tag_name: str, pv: '    
 'cs.sqltags.PolyValue)`\n'    
 '\n'    
 'Convert an SQL `PolyValue` to a tag value.\n'    
 '\n'    
 'This can be overridden by subclasses along with `to_polyvalue`.\n'    
 'The `tag_name` is provided for context\n'    
 'in case it should influence the normalisation.\n'    
 '\n'    
 '### Property `SQLTagSet.name`\n'    
 '\n'    
 'Return the `.name`.\n'    
 '\n'    
 "### Method `SQLTagSet.parent_tagset(self, tag_name='parent')`\n"    
 '\n'    
 'Return the parent `TagSet` as defined by a `Tag`,\n'    
 "by default the `Tag` named `'parent'`.\n"    
 '\n'    
 '### Method `SQLTagSet.to_js_str(tag_name: str, tag_value) -> str`\n'    
 '\n'    
 'Convert `tag_value` to a `str` suitable for storage in `structure_value`.\n'    
 'This can be reversed by `from_js_str`.\n'    
 '\n'    
 'Subclasses wanting extra type support\n'    
 'should either:\n'    
 '(usual approach) provide their own `TYPE_JS_MAPPING` class attribute\n'    
 'as described at the top of this class\n'    
 'or (for unusual requirements) override this method and also `from_js_str`.\n'    
 '\n'    
 '### Method `SQLTagSet.to_polyvalue(tag_name: str, tag_value) -> '    
 'cs.sqltags.PolyValue`\n'    
 '\n'    
 'Normalise `Tag` values for storage via SQL.\n'    
 'Preserve things directly expressable in JSON.\n'    
 'Convert other values via `to_js_str`.\n'    
 'Return `PolyValue` for use with the SQL rows.\n'    
 '\n'    
 '## Class '    
 '`SQLTagsORM(cs.sqlalchemy_utils.ORM,cs.resources.MultiOpenMixin,cs.context.ContextManagerMixin,cs.dateutils.UNIXTimeMixin)`\n'    
 '\n'    
 'The ORM for an `SQLTags`.\n'    
 '\n'    
 'The current implementation uses 3 tables:\n'    
 '* `entities`: this has a NULLable `name`\n'    
 '  and `unixtime` UNIX timestamp;\n'    
 '  this is unique per `name` if the name is not NULL\n'    
 '* `tags`: this has an `entity_id`, `name` and a value stored\n'    
 '  in one of three columns: `float_value`, `string_value` and\n'    
 '  `structured_value` which is a JSON blob;\n'    
 '  this is unique per `(entity_id,name)`\n'    
 '* `tag_subvalues`: this is a broken out version of `tags`\n'    
 '  when `structured_value` is a sequence or mapping,\n'    
 '  breaking out the values one per row;\n'    
 '  this exists to support "tag contains value" lookups\n'    
 '\n'    
 'Tag values are stored as follows:\n'    
 '* `None`: all 3 columns are set to `NULL`\n'    
 '* `float`: stored in `float_value`\n'    
 '* `int`: if the `int` round trips to `float`\n'    
 '  then it is stored in `float_value`,\n'    
 '  otherwise it is stored in `structured_value`\n'    
 '  with the type label `"bigint"`\n'    
 '* `str`: stored in `string_value`\n'    
 '* `list`, `tuple`, `dict`: stored in `structured_value`;\n'    
 '  if these containers contain unJSONable content there will be trouble\n'    
 '* other types, such as `datetime`:\n'    
 '  these are converted to strings with identifying type label prefixes\n'    
 '  and stored in `structured_value`\n'    
 '\n'    
 'The `float_value` and `string_value` columns\n'    
 'allow us to provide indices for these kinds of tag values.\n'    
 '\n'    
 'The type label scheme takes advantage of the fact that actual `str`s\n'    
 'are stored in the `string_value` column.\n'    
 'Because of this, there will be no actual strings in `structured_value`.\n'    
 'Therefore, we can convert nonJSONable types to `str` and store them here.\n'    
 '\n'    
 'The scheme used is to provide conversion functions to convert types\n'    
 'to `str` and back, and an associated "type label" prefix.\n'    
 'For example, we store a `datetime` as the ISO format of the `datetime`\n'    
 'with `"datetime:"` prefixed to it.\n'    
 '\n'    
 'The actual conversions are kept with the `SQLTagSet` class\n'    
 '(or any subclass).\n'    
 'This ORM receives the 3-tuples of SQL ready values\n'    
 'from that class as the `PolyValue` `namedtuple`\n'    
 'and does not perform any conversion itself.\n'    
 'The conversion process is described in `SQLTagSet`.\n'    
 '\n'    
 '### Method `SQLTagsORM.declare_schema(self)`\n'    
 '\n'    
 'Define the database schema / ORM mapping.\n'    
 '\n'    
 '### Method `SQLTagsORM.define_schema(self)`\n'    
 '\n'    
 'Instantiate the schema and define the root metanode.\n'    
 '\n'    
 '### Method `SQLTagsORM.prepare_metanode(self, *, session)`\n'    
 '\n'    
 'Ensure row id 0, the metanode, exists.\n'    
 '\n'    
 '### Method `SQLTagsORM.search(self, *a, **kw)`\n'    
 '\n'    
 'Construct a query to match `Entity` rows\n'    
 'matching the supplied `criteria` iterable.\n'    
 'Return an SQLAlchemy `Query`.\n'    
 '\n'    
 'The `mode` parameter has the following values:\n'    
 "* `'id'`: the query only yields entity ids\n"    
 "* `'entity'`: (default) the query yields entities without tags\n"    
 "* `'tagged'`: (default) the query yields entities left\n"    
 'outer joined with their matching tags\n'    
 '\n'    
 "Note that the `'tagged'` result produces multiple rows for any\n"    
 'entity with multiple tags, and that this requires the caller to\n'    
 'fold entities with multiple tags together.\n'    
 '\n'    
 '*Note*:\n'    
 'due to implementation limitations\n'    
 'the SQL query itself may not apply all the criteria,\n'    
 'so every criterion must still be applied\n'    
 'to the results\n'    
 'using its `.match_entity` method.\n'    
 '\n'    
 'If `name` is omitted or `None` the query will match log entities\n'    
 'otherwise the entity with the specified `name`.\n'    
 '\n'    
 'The `criteria` should be an iterable of `SQTCriterion` instances\n'    
 'used to construct the query.\n'    
 '\n'    
 '## Class `SQTCriterion(cs.tagset.TagSetCriterion)`\n'    
 '\n'    
 'Subclass of `TagSetCriterion` requiring an `.sql_parameters` method\n'    
 'which returns an `SQLParameters` providing the information required\n'    
 'to construct an sqlalchemy query.\n'    
 'It also resets `.CRITERION_PARSE_CLASSES`, which will pick up\n'    
 'the SQL capable criterion classes below.\n'    
 '\n'    
 '### `SQTCriterion.TAG_BASED_TEST_CLASS`\n'    
 '\n'    
 '### Method `SQTCriterion.match_tagged_entity(self, te: cs.tagset.TagSet) -> '    
 'bool`\n'    
 '\n'    
 'Perform the criterion test on the Python object directly.\n'    
 'This is used at the end of a query to implement tests which\n'    
 'cannot be sufficiently implemented in SQL.\n'    
 'If `self.SQL_COMPLETE` it is not necessary to call this method.\n'    
 '\n'    
 '### Method `SQTCriterion.sql_parameters(self, orm) -> '    
 'cs.sqltags.SQLParameters`\n'    
 '\n'    
 'Subclasses must return am `SQLParameters` instance\n'    
 'parameterising the SQL queries that follow.\n'    
 '\n'    
 '## Class `SQTEntityIdTest(SQTCriterion,cs.tagset.TagSetCriterion)`\n'    
 '\n'    
 'A test on `entity.id`.\n'    
 '\n'    
 '### Method `SQTEntityIdTest.match_tagged_entity(self, te: cs.tagset.TagSet) '    
 '-> bool`\n'    
 '\n'    
 'Test the `TagSet` `te` against `self.entity_ids`.\n'    
 '\n'    
 '### Method `SQTEntityIdTest.parse(s, offset=0, delim=None)`\n'    
 '\n'    
 'Parse a decimal entity id from `s`.\n'    
 '\n'    
 '## Function `verbose(msg, *a)`\n'    
 '\n'    
 'Emit message if in verbose mode.\n'    
 '\n'    
 '# Release Log\n'    
 '\n'    
 '\n'    
 '\n'    
 '*Release 20211212*:\n'    
 '* Rename edit_many to edit_tagsets for clarity.\n'    
 '* Small bugfixes.\n'    
 '\n'    
 '*Release 20210913*:\n'    
 '* SQLTagsCommand: rename cmd_ns to cmd_list,cmd_ls.\n'    
 '* SQLTagsCommand.cmd_export: accept "-F export_format" for csv or fstags '    
 'export, accept no criteria to mean all tagsets.\n'    
 '* Encoding schema for nonJSONable types.\n'    
 '* Rename the TagSets abstract base class to BaseTagSets.\n'    
 '* BaseSQLTagsCommand.cmd_edit: implement rename.\n'    
 '* Many other internal small changes.\n'    
 '\n'    
 '*Release 20210420*:\n'    
 '* New PolyValueMixin pulled out of Tags for common support of the '    
 '(float_value,string_value,structured_value).\n'    
 '* SQLTagsORM: new TagSubValues relation containing broken out values for '    
 'values which are sequences, to support efficient lookup if sequence values '    
 'such as log entry categories.\n'    
 '* New BaseSQLTagsCommand.parse_categories static method to parse FOO,BAH '    
 "into ['foo','bah'].\n"    
 '* sqltags find: change default format to "{datetime} {headline}".\n'    
 '* Assorted small changes.\n'    
 '\n'    
 '*Release 20210404*:\n'    
 '* SQLTags.__getitem__: when autocreating an entity, do it in a new session '    
 'so that the entity is commited to the database before any further use.\n'    
 '* SQLTagsCommand: new cmd_dbshell to drop you into the database.\n'    
 '\n'    
 '*Release 20210321*:\n'    
 'Drop logic now merged with cs.sqlalchemy_utils, use the new default session '    
 'stuff.\n'    
 '\n'    
 '*Release 20210306.1*:\n'    
 'Docstring updates.\n'    
 '\n'    
 '*Release 20210306*:\n'    
 'Initial release.'),
  install_requires = ['cs.cmdutils>=20210404', 'cs.context', 'cs.dateutils', 'cs.deco', 'cs.lex', 'cs.logutils', 'cs.obj', 'cs.pfx', 'cs.sqlalchemy_utils>=20210420', 'cs.tagset>=20211212', 'cs.threads>=20201025', 'cs.upd', 'icontract', 'sqlalchemy', 'typeguard'],
  classifiers = ['Programming Language :: Python', '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+)'],
  entry_points = {'console_scripts': ['sqltags = cs.sqltags:main']},
  keywords = ['python3'],
  license = 'GNU General Public License v3 or later (GPLv3+)',
  long_description_content_type = 'text/markdown',
  package_dir = {'': 'lib/python'},
  py_modules = ['cs.sqltags'],
)
