cs.tagset


Namecs.tagset JSON
Version 20240422.2 PyPI version JSON
download
home_pageNone
SummaryTags and sets of tags with __format__ support and optional ontology information.
upload_time2024-04-22 06:32:54
maintainerNone
docs_urlNone
authorNone
requires_pythonNone
licenseGNU General Public License v3 or later (GPLv3+)
keywords python3
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            Tags and sets of tags
with __format__ support and optional ontology information.

*Latest release 20240422.2*:
jsonable: use obj.for_json() if available.

See `cs.fstags` for support for applying these to filesystem objects
such as directories and files.

See `cs.sqltags` for support for databases of entities with tags,
not directly associated with filesystem objects.
This is suited to both log entries (entities with no "name")
and large collections of named entities;
both accept `Tag`s and can be searched on that basis.

All of the available complexity is optional:
you can use `Tag`s without bothering with `TagSet`s
or `TagsOntology`s.

This module contains the following main classes:
* `Tag`: an object with a `.name` and optional `.value` (default `None`)
  and also an optional reference `.ontology`
  for associating semantics with tag values.
  The `.value`, if not `None`, will often be a string,
  but may be any Python object.
  If you're using these via `cs.fstags`,
  the object will need to be JSON transcribeable.
* `TagSet`: a `dict` subclass representing a set of `Tag`s
  to associate with something;
  it also has setlike `.add` and `.discard` methods.
  As such it only supports a single `Tag` for a given tag name,
  but that tag value can of course be a sequence or mapping
  for more elaborate tag values.
* `TagsOntology`:
  a mapping of type names to `TagSet`s defining the type
  and also to entries for the metadata for specific per-type values.

Here's a simple example with some `Tag`s and a `TagSet`.

    >>> tags = TagSet()
    >>> # add a "bare" Tag named 'blue' with no value
    >>> tags.add('blue')
    >>> # add a "topic=tagging" Tag
    >>> tags.set('topic', 'tagging')
    >>> # make a "subtopic" Tag and add it
    >>> subtopic = Tag('subtopic', 'ontologies')
    >>> tags.add(subtopic)
    >>> # Tags have nice repr() and str()
    >>> subtopic
    Tag(name='subtopic',value='ontologies')
    >>> print(subtopic)
    subtopic=ontologies
    >>> # a TagSet also has a nice repr() and str()
    >>> tags
    TagSet:{'blue': None, 'topic': 'tagging', 'subtopic': 'ontologies'}
    >>> print(tags)
    blue subtopic=ontologies topic=tagging
    >>> tags2 = TagSet({'a': 1}, b=3, c=[1,2,3], d='dee')
    >>> tags2
    TagSet:{'a': 1, 'b': 3, 'c': [1, 2, 3], 'd': 'dee'}
    >>> print(tags2)
    a=1 b=3 c=[1,2,3] d=dee
    >>> # since you can print a TagSet to a file as a line of text
    >>> # you can get it back from a line of text
    >>> TagSet.from_line('a=1 b=3 c=[1,2,3] d=dee')
    TagSet:{'a': 1, 'b': 3, 'c': [1, 2, 3], 'd': 'dee'}
    >>> # because TagSets are dicts you can format strings with them
    >>> print('topic:{topic} subtopic:{subtopic}'.format_map(tags))
    topic:tagging subtopic:ontologies
    >>> # TagSets have convenient membership tests
    >>> # test for blueness
    >>> 'blue' in tags
    True
    >>> # test for redness
    >>> 'red' in tags
    False
    >>> # test for any "subtopic" tag
    >>> 'subtopic' in tags
    True
    >>> # test for subtopic=ontologies
    >>> print(subtopic)
    subtopic=ontologies
    >>> subtopic in tags
    True
    >>> # test for subtopic=libraries
    >>> subtopic2 = Tag('subtopic', 'libraries')
    >>> subtopic2 in tags
    False

## Ontologies

`Tag`s and `TagSet`s suffice to apply simple annotations to things.
However, an ontology brings meaning to those annotations.

See the `TagsOntology` class for implementation details,
access methods and more examples.

Consider a record about a movie, with these tags (a `TagSet`):

    title="Avengers Assemble"
    series="Avengers (Marvel)"
    cast={"Scarlett Johansson":"Black Widow (Marvel)"}

where we have the movie title,
a name for the series in which it resides,
and a cast as an association of actors with roles.

An ontology lets us associate implied types and metadata with these values.

Here's an example ontology supporting the above `TagSet`:

    type.cast type=dict key_type=person member_type=character description="members of a production"
    type.character description="an identified member of a story"
    type.series type=str
    character.marvel.black_widow type=character names=["Natasha Romanov"]
    person.scarlett_johansson fullname="Scarlett Johansson" bio="Known for Black Widow in the Marvel stories."

The type information for a `cast`
is defined by the ontology entry named `type.cast`,
which tells us that a `cast` `Tag` is a `dict`,
whose keys are of type `person`
and whose values are of type `character`.
(The default type is `str`.)

To find out the underlying type for a `character`
we look that up in the ontology in turn;
because it does not have a specified `type` `Tag`, it it taken to be a `str`.

Having the types for a `cast`,
it is now possible to look up the metadata for the described cast members.

The key `"Scarlett Johansson"` is a `person`
(from the type definition of `cast`).
The ontology entry for her is named `person.scarlett_johansson`
which is computed as:
* `person`: the type name
* `scarlett_johansson`: obtained by downcasing `"Scarlett Johansson"`
  and replacing whitespace with an underscore.
  The full conversion process is defined
  by the `TagsOntology.value_to_tag_name` function.

The key `"Black Widow (Marvel)"` is a `character`
(again, from the type definition of `cast`).
The ontology entry for her is named `character.marvel.black_widow`
which is computed as:
* `character`: the type name
* `marvel.black_widow`: obtained by downcasing `"Black Widow (Marvel)"`,
  replacing whitespace with an underscore,
  and moving a bracketed suffix to the front as an unbracketed prefix.
  The full conversion process is defined
  by the `TagsOntology.value_to_tag_name` function.

## Format Strings

You can just use `str.format_map` as shown above
for the direct values in a `TagSet`,
since it subclasses `dict`.

However, `TagSet`s also subclass `cs.lex.FormatableMixin`
and therefore have a richer `format_as` method which has an extended syntax
for the format component.
Command line tools like `fstags` use this for output format specifications.

An example:

    >>> # an ontology specifying the type for a colour
    >>> # and some information about the colour "blue"
    >>> ont = TagsOntology(
    ...   {
    ...       'type.colour':
    ...       TagSet(description="a colour, a hue", type="str"),
    ...       'colour.blue':
    ...       TagSet(
    ...           url='https://en.wikipedia.org/wiki/Blue',
    ...           wavelengths='450nm-495nm'
    ...       ),
    ...   }
    ... )
    >>> # tag set with a "blue" tag, using the ontology above
    >>> tags = TagSet(colour='blue', labels=['a', 'b', 'c'], size=9, _ontology=ont)
    >>> tags.format_as('The colour is {colour}.')
    'The colour is blue.'
    >>> # format a string about the tags showing some metadata about the colour
    >>> tags.format_as('Information about the colour may be found here: {colour:metadata.url}')
    'Information about the colour may be found here: https://en.wikipedia.org/wiki/Blue'

## Function `as_unixtime(tag_value)`

Convert a tag value to a UNIX timestamp.

This accepts `int`, `float` (already a timestamp)
and `date` or `datetime`
(use `datetime.timestamp() for a nonnaive `datetime`,
otherwise `time.mktime(tag_value.time_tuple())`,
which assumes the local time zone).

## Class `BaseTagSets(cs.resources.MultiOpenMixin, collections.abc.MutableMapping)`

Base class for collections of `TagSet` instances
such as `cs.fstags.FSTags` and `cs.sqltags.SQLTags`.

Examples of this include:
* `cs.cdrip.MBSQLTags`: a mapping of MusicbrainsNG entities to their associated `TagSet`
* `cs.fstags.FSTags`: a mapping of filesystem paths to their associated `TagSet`
* `cs.sqltags.SQLTags`: a mapping of names to `TagSet`s stored in an SQL database

Subclasses must implement:
* `get(name,default=None)`: return the `TagSet` associated
  with `name`, or `default`.
* `__setitem__(name,tagset)`: associate a `TagSet`with the key `name`;
  this is called by the `__missing__` method with a newly created `TagSet`.
* `keys(self)`: return an iterable of names

Subclasses may reasonably want to override the following:
* `startup_shutdown(self)`: context manager to allocate and release any
  needed resources such as database connections

Subclasses may implement:
* `__len__(self)`: return the number of names

The `TagSet` factory used to fetch or create a `TagSet` is
named `TagSetClass`. The default implementation honours two
class attributes:
* `TAGSETCLASS_DEFAULT`: initially `TagSet`
* `TAGSETCLASS_PREFIX_MAPPING`: a mapping of type names to `TagSet` subclasses

The type name of a `TagSet` name is the first dotted component.
For example, `artist.nick_cave` has the type name `artist`.
A subclass of `BaseTagSets` could utiliise an `ArtistTagSet` subclass of `TagSet`
and provide:

    TAGSETCLASS_PREFIX_MAPPING = {
      'artist': ArtistTagSet,
    }

in its class definition. Accesses to `artist.`* entities would
result in `ArtistTagSet` instances and access to other enitities
would result in ordinary `TagSet` instances.

*Method `BaseTagSets.__init__(self, *, ontology=None)`*:
Initialise the collection.

*`BaseTagSets.TAGSETCLASS_DEFAULT`*

*Method `BaseTagSets.TagSetClass(self, *, name, **kw)`*:
Factory to create a new `TagSet` from `name`.

*Method `BaseTagSets.__contains__(self, name: str)`*:
Test whether `name` is present in the underlying mapping.

*Method `BaseTagSets.__getitem__(self, name: str)`*:
Obtain the `TagSet` associated with `name`.

If `name` is not presently mapped,
return `self.__missing__(name)`.

*Method `BaseTagSets.__iter__(self)`*:
Iteration returns the keys.

*Method `BaseTagSets.__len__(self)`*:
Return the length of the underlying mapping.

*Method `BaseTagSets.__missing__(self, name: str, **kw)`*:
Like `dict`, the `__missing__` method may autocreate a new `TagSet`.

This is called from `__getitem__` if `name` is missing
and uses the factory `cls.default_factory`.
If that is `None` raise `KeyError`,
otherwise call `self.default_factory(name,**kw)`.
If that returns `None` raise `KeyError`,
otherwise save the entity under `name` and return the entity.

*Method `BaseTagSets.__setitem__(self, name, te)`*:
Save `te` in the backend under the key `name`.

*Method `BaseTagSets.add(self, name: str, **kw)`*:
Return a new `TagSet` associated with `name`,
which should not already be in use.

*Method `BaseTagSets.default_factory(self, name: str)`*:
Create a new `TagSet` named `name`.

*Method `BaseTagSets.edit(self, *, select_tagset=None, **kw)`*:
Edit the `TagSet`s.

Parameters:
* `select_tagset`: optional callable accepting a `TagSet`
  which tests whether it should be included in the `TagSet`s
  to be edited
Other keyword arguments are passed to `Tag.edit_tagsets`.

*Method `BaseTagSets.get(self, name: str, default=None)`*:
Return the `TagSet` associated with `name`,
or `default` if there is no such entity.

*Method `BaseTagSets.items(self, *, prefix=None)`*:
Generator yielding `(key,value)` pairs,
optionally constrained to keys starting with `prefix+'.'`.

*Method `BaseTagSets.keys(self, *, prefix=None)`*:
Return the keys starting with `prefix+'.'`
or all keys if `prefix` is `None`.

*Method `BaseTagSets.subdomain(self, subname: str)`*:
Return a proxy for this `BaseTagSets` for the `name`s
starting with `subname+'.'`.

*Method `BaseTagSets.values(self, *, prefix=None)`*:
Generator yielding the mapping values (`TagSet`s),
optionally constrained to keys starting with `prefix+'.'`.

## Function `jsonable(obj, converted: dict)`

Convert `obj` to a JSON encodable form.
This returns `obj` for purely JSONable objects and a JSONable
deep copy of `obj` if it or some subcomponent required
conversion.
`converted` is a dict mapping object ids to their converted forms
to prevent loops.

## Class `MappingTagSets(BaseTagSets)`

A `BaseTagSets` subclass using an arbitrary mapping.

If no mapping is supplied, a `dict` is created for the purpose.

Example:

    >>> tagsets = MappingTagSets()
    >>> list(tagsets.keys())
    []
    >>> tagsets.get('foo')
    >>> tagsets['foo'] = TagSet(bah=1, zot=2)
    >>> list(tagsets.keys())
    ['foo']
    >>> tagsets.get('foo')
    TagSet:{'bah': 1, 'zot': 2}
    >>> list(tagsets.keys(prefix='foo'))
    ['foo']
    >>> list(tagsets.keys(prefix='bah'))
    []

*Method `MappingTagSets.__delitem__(self, name)`*:
Delete the `TagSet` named `name`.

*Method `MappingTagSets.__setitem__(self, name, te)`*:
Save `te` in the backend under the key `name`.

*Method `MappingTagSets.keys(self, *, prefix: Optional[str] = None)`*:
Return an iterable of the keys commencing with `prefix`
or all keys if `prefix` is `None`.

## Class `RegexpTagRule`

A regular expression based `Tag` rule.

This applies a regular expression to a string
and returns inferred `Tag`s.

*Method `RegexpTagRule.infer_tags(self, s)`*:
Apply the rule to the string `s`, return a list of `Tag`s.

## Function `selftest(argv)`

Run some ad hoc self tests.

## Class `Tag(Tag, cs.lex.FormatableMixin)`

A `Tag` has a `.name` (`str`) and a `.value`
and an optional `.ontology`.

The `name` must be a dotted identifier.

Terminology:
* A "bare" `Tag` has a `value` of `None`.
* A "naive" `Tag` has an `ontology` of `None`.

The constructor for a `Tag` is unusual:
* both the `value` and `ontology` are optional,
  defaulting to `None`
* if `name` is a `str` then we always construct a new `Tag`
  with the suppplied values
* if `name` is not a `str`
  it should be a `Tag`like object to promote;
  it is an error if the `value` parameter is not `None`
  in this case
* an optional `prefix` may be supplied
  which is prepended to `name` with a dot (`'.'`) if not empty

The promotion process is as follows:
* if `name` is a `Tag` subinstance
  then if the supplied `ontology` is not `None`
  and is not the ontology associated with `name`
  then a new `Tag` is made,
  otherwise the original `Tag` is returned unchanged
* otherwise a new `Tag` is made from `name`
  using its `.value`
  and overriding its `.ontology`
  if the `ontology` parameter is not `None`

Examples:

    >>> ont = TagsOntology({'colour.blue': TagSet(wavelengths='450nm-495nm')})
    >>> tag0 = Tag('colour', 'blue')
    >>> tag0
    Tag(name='colour',value='blue')
    >>> tag = Tag(tag0)
    >>> tag
    Tag(name='colour',value='blue')
    >>> tag = Tag(tag0, ontology=ont)
    >>> tag # doctest: +ELLIPSIS
    Tag(name='colour',value='blue',ontology=...)
    >>> tag = Tag(tag0, prefix='surface')
    >>> tag
    Tag(name='surface.colour',value='blue')

*Method `Tag.__init__(self, *a, **kw)`*:
Dummy `__init__` to avoid `FormatableMixin.__init__`
because we subclass `namedtuple` which has no `__init__`.

*`Tag.__hash__`*

*Method `Tag.__str__(self)`*:
Encode `name` and `value`.

*Method `Tag.alt_values(self, value_tag_name=None)`*:
Return a list of alternative values for this `Tag`
on the premise that each has a metadata entry.

*Property `Tag.basetype`*:
The base type name for this tag.
Returns `None` if there is no ontology.

This calls `self.onotology.basetype(self.name)`.
The basetype is the endpoint of a cascade down the defined types.

For example, this might tell us that a `Tag` `role="Fred"`
has a basetype `"str"`
by cascading through a hypothetical chain `role`->`character`->`str`:

    type.role type=character
    type.character type=str

*Method `Tag.from_arg(arg, offset=0, ontology=None)`*:
Parse a `Tag` from the string `arg` at `offset` (default `0`).
where `arg` is known to be entirely composed of the value,
such as a command line argument.

This calls the `from_str` method with `fallback_parse` set
to gather then entire tail of the supplied string `arg`.

*Method `Tag.from_str(s, offset=0, ontology=None, fallback_parse=None)`*:
Parse a `Tag` definition from `s` at `offset` (default `0`).

*Method `Tag.from_str2(s, offset=0, *, ontology=None, extra_types=None, fallback_parse=None)`*:
Parse tag_name[=value], return `(Tag,offset)`.

*Method `Tag.is_valid_name(name)`*:
Test whether a tag name is valid: a dotted identifier.

*Method `Tag.key_metadata(self, key)`*:
Return the metadata definition for `key`.

The metadata `TagSet` is obtained from the ontology entry
*type*`.`*key_tag_name*
where *type* is the `Tag`'s `key_type`
and *key_tag_name* is the key converted
into a dotted identifier by `TagsOntology.value_to_tag_name`.

*Property `Tag.key_type`*:
The type name for members of this tag.

This is required if `.value` is a mapping.

*Property `Tag.key_typedef`*:
The typedata definition for this `Tag`'s keys.

This is for `Tag`s which store mappings,
for example a movie cast, mapping actors to roles.

The name of the member type comes from
the `key_type` entry from `self.typedata`.
That name is then looked up in the ontology's types.

*Method `Tag.matches(self, tag_name, value)`*:
Test whether this `Tag` matches `(tag_name,value)`.

*Method `Tag.member_metadata(self, member_key)`*:
Return the metadata definition for self[member_key].

The metadata `TagSet` is obtained from the ontology entry
*type*`.`*member_tag_name*
where *type* is the `Tag`'s `member_type`
and *member_tag_name* is the member value converted
into a dotted identifier by `TagsOntology.value_to_tag_name`.

*Property `Tag.member_type`*:
The type name for members of this tag.

This is required if `.value` is a sequence or mapping.

*Property `Tag.member_typedef`*:
The typedata definition for this `Tag`'s members.

This is for `Tag`s which store mappings or sequences,
for example a movie cast, mapping actors to roles,
or a list of scenes.

The name of the member type comes from
the `member_type` entry from `self.typedata`.
That name is then looked up in the ontology's types.

*Property `Tag.meta`*:
Shortcut property for the metadata `TagSet`.

*Method `Tag.metadata(self, *, ontology=None, convert=None)`*:
Fetch the metadata information about this specific tag value,
derived through the `ontology` from the tag name and value.
The default `ontology` is `self.ontology`.

For a scalar type (`int`, `float`, `str`) this is the ontology `TagSet`
for `self.value`.

For a sequence (`list`) this is a list of the metadata
for each member.

For a mapping (`dict`) this is mapping of `key->metadata`.

*Method `Tag.parse_name(s, offset=0)`*:
Parse a tag name from `s` at `offset`: a dotted identifier.

*Method `Tag.parse_value(s, offset=0, *, extra_types=None, fallback_parse=None)`*:
Parse a value from `s` at `offset` (default `0`).
Return the value, or `None` on no data.

The optional `extra_types` parameter may be an iterable of
`(type,from_str,to_str)` tuples where `from_str` is a
function which takes a string and returns a Python object
(expected to be an instance of `type`).
The default comes from `cls.EXTRA_TYPES`.
This supports storage of nonJSONable values in text form.

The optional `fallback_parse` parameter
specifies a parse function accepting `(s,offset)`
and returning `(parsed,new_offset)`
where `parsed` is text from `s[offset:]`
and `new_offset` is where the parse stopped.
The default is `cs.lex.get_nonwhite`
to gather nonwhitespace characters,
intended to support *tag_name*`=`*bare_word*
in human edited tag files.

The core syntax for values is JSON;
value text commencing with any of `'"'`, `'['` or `'{'`
is treated as JSON and decoded directly,
leaving the offset at the end of the JSON parse.

Otherwise all the nonwhitespace at this point is collected
as the value text,
leaving the offset at the next whitespace character
or the end of the string.
The text so collected is then tried against the `from_str`
function of each `extra_types`;
the first successful parse is accepted as the value.
If no extra type match,
the text is tried against `int()` and `float()`;
if one of these parses the text and `str()` of the result round trips
to the original text
then that value is used.
Otherwise the text itself is kept as the value.

*Method `Tag.transcribe_value(value, extra_types=None, json_options=None)`*:
Transcribe `value` for use in `Tag` transcription.

The optional `extra_types` parameter may be an iterable of
`(type,from_str,to_str)` tuples where `to_str` is a
function which takes a string and returns a Python object
(expected to be an instance of `type`).
The default comes from `cls.EXTRA_TYPES`.

If `value` is an instance of `type`
then the `to_str` function is used to transcribe the value
as a `str`, which should not include any whitespace
(because of the implementation of `parse_value`).
If there is no matching `to_str` function,
`cls.JSON_ENCODER.encode` is used to transcribe `value`.

This supports storage of nonJSONable values in text form.

*Property `Tag.typedef`*:
The defining `TagSet` for this tag's name.

This is how its type is defined,
and is obtained from:
`self.ontology['type.'+self.name]`

Basic `Tag`s often do not need a type definition;
these are only needed for structured tag values
(example: a mapping of cast members)
or when a `Tag` name is an alias for another type
(example: a cast member name might be an `actor`
which in turn might be a `person`).

For example, a `Tag` `colour=blue`
gets its type information from the `type.colour` entry in an ontology;
that entry is just a `TagSet` with relevant information.

## Function `tag_or_tag_value(*da, **dkw)`

A decorator for functions or methods which may be called as:

    func(name[,value])

or as:

    func(Tag)

The optional decorator argument `no_self` (default `False`)
should be supplied for plain functions
as they have no leading `self` parameter to accomodate.

Example:

    @tag_or_tag_value
    def add(self, tag_name, value, *, verbose=None):

This defines a `.add()` method
which can be called with `name` and `value`
or with single `Tag`like object
(something with `.name` and `.value` attributes),
for example:

    tags = TagSet()
    ....
    tags.add('colour', 'blue')
    ....
    tag = Tag('size', 9)
    tags.add(tag)

## Class `TagBasedTest(TagBasedTest, TagSetCriterion)`

A test based on a `Tag`.

Attributes:
* `spec`: the source text from which this choice was parsed,
  possibly `None`
* `choice`: the apply/reject flag
* `tag`: the `Tag` representing the criterion
* `comparison`: an indication of the test comparison

The following comparison values are recognised:
* `None`: test for the presence of the `Tag`
* `'='`: test that the tag value equals `tag.value`
* `'<'`: test that the tag value is less than `tag.value`
* `'<='`: test that the tag value is less than or equal to `tag.value`
* `'>'`: test that the tag value is greater than `tag.value`
* `'>='`: test that the tag value is greater than or equal to `tag.value`
* `'~/'`: test if the tag value as a regexp is present in `tag.value`
* '~': test if a matching tag value is present in `tag.value`

*Method `TagBasedTest.by_tag_value(tag_name, tag_value, *, choice=True, comparison='=')`*:
Return a `TagBasedTest` based on a `Tag` or `tag_name,tag_value`.

*Method `TagBasedTest.match_tagged_entity(self, te: 'TagSet') -> bool`*:
Test against the `Tag`s in `tags`.

*Note*: comparisons when `self.tag.name` is not in `tags`
always return `False` (possibly inverted by `self.choice`).

*Method `TagBasedTest.parse(s, offset=0, delim=None)`*:
Parse *tag_name*[{`<`|`<=`|'='|'>='|`>`|'~'}*value*]
and return `(dict,offset)`
where the `dict` contains the following keys and values:
* `tag`: a `Tag` embodying the tag name and value
* `comparison`: an indication of the test comparison

## Class `TagFile(cs.fs.FSPathBasedSingleton, BaseTagSets)`

A reference to a specific file containing tags.

This manages a mapping of `name` => `TagSet`,
itself a mapping of tag name => tag value.

*Method `TagFile.__setitem__(self, name, te)`*:
Set item `name` to `te`.

*Method `TagFile.get(self, name, default=None)`*:
Get from the tagsets.

*Method `TagFile.is_modified(self)`*:
Test whether this `TagSet` has been modified.

*Method `TagFile.keys(self, *, prefix=None)`*:
`tagsets.keys`

If the options `prefix` is supplied,
yield only those keys starting with `prefix`.

*Method `TagFile.load_tagsets(filepath, ontology, extra_types=None)`*:
Load `filepath` and return `(tagsets,unparsed)`.

The returned `tagsets` are a mapping of `name`=>`tag_name`=>`value`.
The returned `unparsed` is a list of `(lineno,line)`
for lines which failed the parse (excluding the trailing newline).

*Property `TagFile.names`*:
The names from this `FSTagsTagFile` as a list.

*Method `TagFile.parse_tags_line(line, ontology=None, verbose=None, extra_types=None) -> Tuple[str, cs.tagset.TagSet]`*:
Parse a "name tags..." line as from a `.fstags` file,
return `(name,TagSet)`.

*Method `TagFile.save(self, extra_types=None, prune=False)`*:
Save the tag map to the tag file if modified.

*Method `TagFile.save_tagsets(filepath, tagsets, unparsed, extra_types=None, prune=False, update_mapping: Optional[Mapping] = None, update_prefix: Optional[str] = None, update_uuid_tag_name: Optional[str] = None)`*:
Save `tagsets` and `unparsed` to `filepath`.

This method will create the required intermediate directories
if missing.

This method *does not* clear the `.modified` attribute of the `TagSet`s
because it does not know it is saving to the `Tagset`'s primary location.

*Method `TagFile.startup_shutdown(self)`*:
Save the tagsets if modified.

*Method `TagFile.tags_line(name, tags, extra_types=None, prune=False)`*:
Transcribe a `name` and its `tags` for use as a `.fstags` file line.

*Property `TagFile.tagsets`*:
The tag map from the tag file,
a mapping of name=>`TagSet`.

This is loaded on demand.

*Method `TagFile.update(self, name, tags, *, prefix=None, verbose=None)`*:
Update the tags for `name` from the supplied `tags`
as for `Tagset.update`.

## Class `TagsCommandMixin`

Utility methods for `cs.cmdutils.BaseCommand` classes working with tags.

Optional subclass attributes:
* `TAGSET_CRITERION_CLASS`: a `TagSetCriterion` duck class,
  default `TagSetCriterion`.
  For example, `cs.sqltags` has a subclass
  with an `.extend_query` method for computing an SQL JOIN
  used in searching for tagged entities.

*`TagsCommandMixin.TagAddRemove`*

*Method `TagsCommandMixin.parse_tag_addremove(arg, offset=0)`*:
Parse `arg` as an add/remove tag specification
of the form [`-`]*tag_name*[`=`*value*].
Return `(remove,Tag)`.

Examples:

    >>> TagsCommandMixin.parse_tag_addremove('a')
    TagAddRemove(remove=False, tag=Tag(name='a',value=None))
    >>> TagsCommandMixin.parse_tag_addremove('-a')
    TagAddRemove(remove=True, tag=Tag(name='a',value=None))
    >>> TagsCommandMixin.parse_tag_addremove('a=1')
    TagAddRemove(remove=False, tag=Tag(name='a',value=1))
    >>> TagsCommandMixin.parse_tag_addremove('-a=1')
    TagAddRemove(remove=True, tag=Tag(name='a',value=1))
    >>> TagsCommandMixin.parse_tag_addremove('-a="foo bah"')
    TagAddRemove(remove=True, tag=Tag(name='a',value='foo bah'))
    >>> TagsCommandMixin.parse_tag_addremove('-a=foo bah')
    TagAddRemove(remove=True, tag=Tag(name='a',value='foo bah'))

*Method `TagsCommandMixin.parse_tag_choices(argv)`*:
Parse `argv` as an iterable of [`!`]*tag_name*[`=`*tag_value`] `Tag`
additions/deletions.

*Method `TagsCommandMixin.parse_tagset_criteria(argv, tag_based_test_class=None)`*:
Parse tag specifications from `argv` until an unparseable item is found.
Return `(criteria,argv)`
where `criteria` is a list of the parsed criteria
and `argv` is the remaining unparsed items.

Each item is parsed via
`cls.parse_tagset_criterion(item,tag_based_test_class)`.

*Method `TagsCommandMixin.parse_tagset_criterion(arg, tag_based_test_class=None)`*:
Parse `arg` as a tag specification
and return a `tag_based_test_class` instance
via its `.from_str` factory method.
Raises `ValueError` in a misparse.
The default `tag_based_test_class`
comes from `cls.TAGSET_CRITERION_CLASS`,
which itself defaults to class `TagSetCriterion`.

The default `TagSetCriterion.from_str` recognises:
* `-`*tag_name*: a negative requirement for *tag_name*
* *tag_name*[`=`*value*]: a positive requirement for a *tag_name*
  with optional *value*.

## Class `TagSet(builtins.dict, cs.dateutils.UNIXTimeMixin, cs.lex.FormatableMixin, cs.mappings.AttrableMappingMixin)`

A setlike class associating a set of tag names with values.

This actually subclasses `dict`, so a `TagSet` is a direct
mapping of tag names to values.
It accepts attribute access to simple tag values when they
do not conflict with the class methods;
the reliable method is normal item access.

*NOTE*: iteration yields `Tag`s, not dict keys.

Also note that all the `Tags` from a `TagSet`
share its ontology.

Subclasses should override the `set` and `discard` methods;
the `dict` and mapping methods
are defined in terms of these two basic operations.

`TagSet`s have a few special properties:
* `id`: a domain specific identifier;
  this may reasonably be `None` for entities
  not associated with database rows;
  the `cs.sqltags.SQLTags` class associates this
  with the database row id.
* `name`: the entity's name;
  a read only alias for the `'name'` `Tag`.
  The `cs.sqltags.SQLTags` class defines "log entries"
  as `TagSet`s with no `name`.
* `unixtime`: a UNIX timestamp,
  a `float` holding seconds since the UNIX epoch
  (midnight, 1 January 1970 UTC).
  This is typically the row creation time
  for entities associated with database rows,
  but usually the event time for `TagSet`s describing an event.

Because ` TagSet` subclasses `cs.mappings.AttrableMappingMixin`
you can also access tag values as attributes
*provided* that they do not conflict with instance attributes
or class methods or properties.

*Method `TagSet.__init__(self, *a, _id=None, _ontology=None, **kw)`*:
Initialise the `TagSet`.

Parameters:
* positional parameters initialise the `dict`
  and are passed to `dict.__init__`
* `_id`: optional identity value for databaselike implementations
* `_ontology`: optional `TagsOntology to use for this `TagSet`
* other alphabetic keyword parameters are also used to initialise the
  `dict` and are passed to `dict.__init__`

*Method `TagSet.__contains__(self, tag)`*:
Test for a tag being in this `TagSet`.

If the supplied `tag` is a `str` then this test
is for the presence of `tag` in the keys.

Otherwise,
for each tag `T` in the tagset
test `T.matches(tag)` and return `True` on success.
The default `Tag.matches` method compares the tag name
and if the same,
returns true if `tag.value` is `None` (basic "is the tag present" test)
and otherwise true if `tag.value==T.value` (basic "tag value equality" test).

Otherwise return `False`.

*Method `TagSet.__getattr__(self, attr)`*:
Support access to dotted name attributes.

The following attribute accesses are supported:
- an attrbute from a superclass
- a `Tag` whose name is `attr`; return its value
- the value of `self.auto_infer(attr)` if that does not raise `ValueError`
- if `self.ontology`, try {type}_{field} and {type}_{field}s
- otherwise return `self.subtags(attr)` to allow access to dotted tags,
  provided any existing tags start with "attr."

If this `TagSet` has an ontology
and `attr looks like *typename*`_`*fieldname*
and *typename* is a key,
look up the metadata for the `Tag` value
and return the metadata's *fieldname* key.
This also works for plural values.

For example if a `TagSet` has the tag `artists=["fred","joe"]`
and `attr` is `artist_names`
then the metadata entries for `"fred"` and `"joe"` are looked up
and their `artist_name` tags are returned,
perhaps resulting in the list
`["Fred Thing","Joe Thang"]`.

If there are keys commencing with `attr+'.'`
then this returns a view of those keys
so that a subsequent attribute access can access one of those keys.

Otherwise, a superclass attribute access is performed.

Example of dotted access to tags like `c.x`:

    >>> tags=TagSet(a=1,b=2)
    >>> tags.a
    1
    >>> tags.c
    Traceback (most recent call last):
        ...
    AttributeError: TagSet.c
    >>> tags['c.z']=9
    >>> tags['c.x']=8
    >>> tags
    TagSet:{'a': 1, 'b': 2, 'c.z': 9, 'c.x': 8}
    >>> tags.c
    TagSetPrefixView:c.{'z': 9, 'x': 8}
    >>> tags.c.z
    9

However, this is not supported when there is a tag named `'c'`
because `tags.c` has to return the `'c'` tag value:

    >>> tags=TagSet(a=1,b=2,c=3)
    >>> tags.a
    1
    >>> tags.c
    3
    >>> tags['c.z']=9
    >>> tags.c.z
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'int' object has no attribute 'z'

*Method `TagSet.__iter__(self, prefix=None, ontology=None)`*:
Yield the tag data as `Tag`s.

*Method `TagSet.__setattr__(self, attr, value)`*:
Attribute based `Tag` access.

If `attr` is private or is in `self.__dict__` then that is updated,
supporting "normal" attributes set on the instance.
Otherwise the `Tag` named `attr` is set to `value`.

The `__init__` methods of subclasses should do something like this
(from `TagSet.__init__`)
to set up the ordinary instance attributes
which are not to be treated as `Tag`s:

    self.__dict__.update(id=_id, ontology=_ontology, modified=False)

*Method `TagSet.__str__(self)`*:
The `TagSet` suitable for writing to a tag file.

*Method `TagSet.add(self, tag_name, value, **kw)`*:
Adding a `Tag` calls the class `set()` method.

*Method `TagSet.as_dict(self)`*:
Return a `dict` mapping tag name to value.

*Method `TagSet.as_tags(self, prefix=None, ontology=None)`*:
Yield the tag data as `Tag`s.

*Property `TagSet.auto`*:
The automatic namespace.
Here we can refer to dotted tag names directly as attributes.

*Method `TagSet.auto_infer(self, attr)`*:
The default inference implementation.

This should return a value if `attr` is inferrable
and raise `ValueError` if not.

The default implementation returns the direct tag value for `attr`
if present.

*Property `TagSet.csvrow`*:
This `TagSet` as a list useful to a `csv.writer`.
The inverse of `from_csvrow`.

*Method `TagSet.discard(self, tag_name, value, *, verbose=None)`*:
Discard the tag matching `(tag_name,value)`.
Return a `Tag` with the old value,
or `None` if there was no matching tag.

Note that if the tag value is `None`
then the tag is unconditionally discarded.
Otherwise the tag is only discarded
if its value matches.

*Method `TagSet.dump(self, keys=None, *, preindent=None, file=None, **pf_kwargs)`*:
Dump a `TagSet` in multiline format.

Parameters:
* `keys`: optional iterable of `Tag` names to print
* `file`: optional keyword parameter specifying the output filelike 
  object; the default is `sys.stdout`.
* `preindent`: optional leading indentation for the entire dump,
  either a `str` or an `int` indicating a number of spaces
Other keyword arguments are passed to `pprint.pformat`.

*Method `TagSet.edit(self, editor=None, verbose=None, comments=())`*:
Edit this `TagSet`.

*Method `TagSet.edit_tagsets(tes, editor=None, verbose=True)`*:
Edit a collection of `TagSet`s.
Return a list of `(old_name,new_name,TagSet)` for those which were modified.

This function supports modifying both `name` and `Tag`s.
The `Tag`s are updated directly.
The changed names are returning in the `old_name,new_name` above.

The collection `tes` may be either a mapping of name/key
to `TagSet` or an iterable of `TagSets`. If the latter, a
mapping is made based on `te.name or te.id` for each item
`te` in the iterable.

*Method `TagSet.from_csvrow(csvrow)`*:
Construct a `TagSet` from a CSV row like that from
`TagSet.csvrow`, being `unixtime,id,name,tags...`.

*Method `TagSet.from_ini(f, section: str, missing_ok=False)`*:
Load a `TagSet` from a section of a `.ini` file.

Parameters:
* `f`: the `.ini` format file to read;
  an iterable of lines (eg a file object)
  or the name of a file to open
* `section`: the name of the config section
  from which to load the `TagSet`
* `missing_ok`: optional flag, default `False`;
  if true a missing file will return an empty `TagSet`
  instead of raising `FileNotFoundError`

*Method `TagSet.from_line(line, offset=0, *, ontology=None, extra_types=None, verbose=None)`*:
Create a new `TagSet` from a line of text.

*Method `TagSet.from_tags(tags, _id=None, _ontology=None)`*:
Make a `TagSet` from an iterable of `Tag`s.

*Method `TagSet.get_arg_name(self, field_name)`*:
Override for `FormattableMixin.get_arg_name`:
return the leading dotted identifier,
which represents a tag or tag prefix.

*Method `TagSet.get_value(self, arg_name, a, kw)`*:
Override for `FormattableMixin.get_value`:
look up `arg_name` in `kw`, return a value.

The value is obtained as follows:
* `kw[arg_name]`: the `Tag` named `arg_name` if present
* `kw.get_format_attribute(arg_name)`:
  a formattable attribute named `arg_name`
otherwise raise `KeyError` if `self.format_mode.strict`
otherwise return the placeholder string `'{'+arg_name+'}'`.

*Method `TagSet.is_stale(self, max_age=None)`*:
Test whether this `TagSet` is stale
i.e. the time since `self.last_updated` UNIX time exceeds `max_age` seconds
(default from `self.STALE_AGE`).

This is a convenience function for `TagSet`s which cache external data.

*Property `TagSet.name`*:
Read only `name` property, `None` if there is no `'name'` tag.

*Method `TagSet.save_as_ini(self, f, section: str, config=None)`*:
Save this `TagSet` to the config file `f` as `section`.

If `f` is a string, read an existing config from that file
and update the section.

*Method `TagSet.set(self, tag_name, value, *, verbose=None)`*:
Set `self[tag_name]=value`.
If `verbose`, emit an info message if this changes the previous value.

*Method `TagSet.set_from(self, other, verbose=None)`*:
Completely replace the values in `self`
with the values from `other`,
a `TagSet` or any other `name`=>`value` dict.

This has the feature of logging changes
by calling `.set` and `.discard` to effect the changes.

*Method `TagSet.subtags(self, prefix, as_tagset=False)`*:
Return `TagSetPrefixView` of the tags commencing with `prefix+'.'`
with the key prefixes stripped off.

If `as_tagset` is true (default `False`)
return a new standalone `TagSet` containing the prefixed keys.

Example:

    >>> tags = TagSet({'a.b':1, 'a.d':2, 'c.e':3})
    >>> tags.subtags('a')
    TagSetPrefixView:a.{'b': 1, 'd': 2}
    >>> tags.subtags('a', as_tagset=True)
    TagSet:{'b': 1, 'd': 2}

*Method `TagSet.tag(self, tag_name, prefix=None, ontology=None)`*:
Return a `Tag` for `tag_name`, or `None` if missing.

Parameters:
* `tag_name`: the name of the `Tag` to create
* `prefix`: optional prefix;
  if supplied, prepend `prefix+'.'` to the `Tag` name
* `ontology`: optional ontology for the `Tag`,
  default `self.ontology`

*Method `TagSet.tag_metadata(self, tag_name, prefix=None, ontology=None, convert=None)`*:
Return a list of the metadata for the `Tag` named `tag_name`,
or an empty list if the `Tag` is missing.

*Property `TagSet.unixtime`*:
`unixtime` property, autosets to `time.time()` if accessed and missing.

*Method `TagSet.update(self, other=None, *, prefix=None, verbose=None, **kw)`*:
Update this `TagSet` from `other`,
a dict of `{name:value}`
or an iterable of `Tag`like or `(name,value)` things.

*Property `TagSet.uuid`*:
The `TagSet`'s `'uuid'` value as a UUID if present, otherwise `None`.

## Class `TagSetCriterion(cs.deco.Promotable)`

A testable criterion for a `TagSet`.

*`TagSetCriterion.TAG_BASED_TEST_CLASS`*

*Method `TagSetCriterion.from_any(o)`*:
Convert some suitable object `o` into a `TagSetCriterion`.

Various possibilities for `o` are:
* `TagSetCriterion`: returned unchanged
* `str`: a string tests for the presence
  of a tag with that name and optional value;
* an object with a `.choice` attribute;
  this is taken to be a `TagSetCriterion` ducktype and returned unchanged
* an object with `.name` and `.value` attributes;
  this is taken to be `Tag`-like and a positive test is constructed
* `Tag`: an object with a `.name` and `.value`
  is equivalent to a positive equality `TagBasedTest`
* `(name,value)`: a 2 element sequence
  is equivalent to a positive equality `TagBasedTest`

*Method `TagSetCriterion.from_arg(arg, fallback_parse=None)`*:
Prepare a `TagSetCriterion` from the string `arg`
where `arg` is known to be entirely composed of the value,
such as a command line argument.

This calls the `from_str` method with `fallback_parse` set
to gather then entire tail of the supplied string `arg`.

*Method `TagSetCriterion.from_str(s: str, fallback_parse=None)`*:
Prepare a `TagSetCriterion` from the string `s`.

*Method `TagSetCriterion.from_str2(s, offset=0, delim=None, fallback_parse=None)`*:
Parse a criterion from `s` at `offset` and return `(TagSetCriterion,offset)`.

This method recognises an optional leading `'!'` or `'-'`
indicating negation of the test,
followed by a criterion recognised by the `.parse` method
of one of the classes in `cls.CRITERION_PARSE_CLASSES`.

*Method `TagSetCriterion.match_tagged_entity(self, te: 'TagSet') -> bool`*:
Apply this `TagSetCriterion` to a `TagSet`.

## Class `TagSetPrefixView(cs.lex.FormatableMixin)`

A view of a `TagSet` via a `prefix`.

Access to a key `k` accesses the `TagSet`
with the key `prefix+'.'+k`.

This is a kind of funny hybrid of a `Tag` and a `TagSet`
in that some things such as `__format__`
will format the `Tag` named `prefix` if it exists
in preference to the subtags.

Example:

    >>> tags = TagSet(a=1, b=2)
    >>> tags
    TagSet:{'a': 1, 'b': 2}
    >>> tags['sub.x'] = 3
    >>> tags['sub.y'] = 4
    >>> tags
    TagSet:{'a': 1, 'b': 2, 'sub.x': 3, 'sub.y': 4}
    >>> sub = tags.sub
    >>> sub
    TagSetPrefixView:sub.{'x': 3, 'y': 4}
    >>> sub.z = 5
    >>> sub
    TagSetPrefixView:sub.{'x': 3, 'y': 4, 'z': 5}
    >>> tags
    TagSet:{'a': 1, 'b': 2, 'sub.x': 3, 'sub.y': 4, 'sub.z': 5}

*Method `TagSetPrefixView.__getattr__(self, attr)`*:
Proxy other attributes through to the `TagSet`.

*Method `TagSetPrefixView.__setattr__(self, attr, value)`*:
Attribute based `Tag` access.

If `attr` is in `self.__dict__` then that is updated,
supporting "normal" attributes set on the instance.
Otherwise the `Tag` named `attr` is set to `value`.

The `__init__` methods of subclasses should do something like this
(from `TagSet.__init__`)
to set up the ordinary instance attributes
which are not to be treated as `Tag`s:

    self.__dict__.update(id=_id, ontology=_ontology, modified=False)

*Method `TagSetPrefixView.as_dict(self)`*:
Return a `dict` representation of this view.

*Method `TagSetPrefixView.get(self, k, default=None)`*:
Mapping `get` method.

*Method `TagSetPrefixView.get_format_attribute(self, attr)`*:
Fetch a formatting attribute from the proxied object.

*Method `TagSetPrefixView.items(self)`*:
Return an iterable of the items (`Tag` name, `Tag`).

*Method `TagSetPrefixView.keys(self)`*:
The keys of the subtags.

*Property `TagSetPrefixView.ontology`*:
The ontology of the references `TagSet`.

*Method `TagSetPrefixView.setdefault(self, k, v=None)`*:
Mapping `setdefault` method.

*Method `TagSetPrefixView.subtags(self, subprefix)`*:
Return a deeper view of the `TagSet`.

*Property `TagSetPrefixView.tag`*:
The `Tag` for the prefix, or `None` if there is no such `Tag`.

*Method `TagSetPrefixView.update(self, mapping)`*:
Update tags from a name->value mapping.

*Property `TagSetPrefixView.value`*:
Return the `Tag` value for the prefix, or `None` if there is no such `Tag`.

*Method `TagSetPrefixView.values(self)`*:
Return an iterable of the values (`Tag`s).

## Class `TagSetsSubdomain(cs.obj.SingletonMixin, cs.mappings.PrefixedMappingProxy)`

A view into a `BaseTagSets` for keys commencing with a prefix
being the subdomain plus a dot (`'.'`).

*Property `TagSetsSubdomain.TAGGED_ENTITY_FACTORY`*:
The entity factory comes from the parent collection.

## Class `TagsOntology(cs.obj.SingletonMixin, BaseTagSets)`

An ontology for tag names.
This is based around a mapping of names
to ontological information expressed as a `TagSet`.

Normally an object's tags are not a self contained repository of all the information;
instead a tag just names some information.

As a example, consider the tag `colour=blue`.
Meta information about `blue` is obtained via the ontology,
which has an entry for the colour `blue`.
We adopt the convention that the type is just the tag name,
so we obtain the metadata by calling `ontology.metadata(tag)`
or alternatively `ontology.metadata(tag.name,tag.value)`
being the type name and value respectively.

The ontology itself is based around `TagSets` and effectively the call
`ontology.metadata('colour','blue')`
would look up the `TagSet` named `colour.blue` in the underlying `Tagsets`.

For a self contained dataset this means that it can be its own ontology.
For tags associated with arbitrary objects
such as the filesystem tags maintained by `cs.fstags`
the ontology would be a separate tags collection stored in a central place.

There are two main categories of entries in an ontology:
* metadata: other entries named *typename*`.`*value_key*
  contains a `TagSet` holding metadata for a value of type *typename*
  whose value is mapped to *value_key*
* types: an optional entry named `type.`*typename* contains a `TagSet`
  describing the type named *typename*;
  really this is just more metadata where the "type name" is `type`

Metadata are `TagSets` instances describing particular values of a type.
For example, some metadata for the `Tag` `colour="blue"`:

    colour.blue url="https://en.wikipedia.org/wiki/Blue" wavelengths="450nm-495nm"

Some metadata associated with the `Tag` `actor="Scarlett Johansson"`:

    actor.scarlett_johansson role=["Black Widow (Marvel)"]
    character.marvel.black_widow fullname=["Natasha Romanov"]

The tag values are lists above because an actor might play many roles, etc.

There's a convention for converting human descriptions
such as the role string `"Black Widow (Marvel)"` to its metadata.
* the value `"Black Widow (Marvel)"` if converted to a key
  by the ontology method `value_to_tag_name`;
  it moves a bracket suffix such as `(Marvel)` to the front as a prefix
  `marvel.` and downcases the rest of the string and turns spaces into underscores.
  This yields the value key `marvel.black_widow`.
* the type is `role`, so the ontology entry for the metadata
  is `role.marvel.black_widow`

This requires type information about a `role`.
Here are some type definitions supporting the above metadata:

    type.person type=str description="A person."
    type.actor type=person description="An actor's stage name."
    type.character type=str description="A person in a story."
    type.role type_name=character description="A character role in a performance."
    type.cast type=dict key_type=actor member_type=role description="Cast members and their roles."

The basic types have their Python names: `int`, `float`, `str`, `list`,
`dict`, `date`, `datetime`.
You can define subtypes of these for your own purposes
as illustrated above.

For example:

    type.colour type=str description="A hue."

which subclasses `str`.

Subtypes of `list` include a `member_type`
specifying the type for members of a `Tag` value:

    type.scene type=list member_type=str description="A movie scene."

Subtypes of `dict` include a `key_type` and a `member_type`
specifying the type for keys and members of a `Tag` value:

Accessing type data and metadata:

A `TagSet` may have a reference to a `TagsOntology` as `.ontology`
and so also do any of its `Tag`s.

*Method `TagsOntology.__bool__(self)`*:
Support easy `ontology or some_default` tests,
since ontologies are broadly optional.

*Method `TagsOntology.__delitem__(self, name)`*:
Delete the entity named `name`.

*Method `TagsOntology.__getitem__(self, name)`*:
Fetch `tags` for the entity named `name`.

*Method `TagsOntology.__setitem__(self, name, tags)`*:
Apply `tags` to the entity named `name`.

*Method `TagsOntology.add_tagsets(self, tagsets: cs.tagset.BaseTagSets, match, unmatch=None, index=0)`*:
Insert a `_TagsOntology_SubTagSets` at `index`
in the list of `_TagsOntology_SubTagSets`es.

The new `_TagsOntology_SubTagSets` instance is initialised
from the supplied `tagsets`, `match`, `unmatch` parameters.

*Method `TagsOntology.as_dict(self)`*:
Return a `dict` containing a mapping of entry names to their `TagSet`s.

*Method `TagsOntology.basetype(self, typename)`*:
Infer the base type name from a type name.
The default type is `'str'`,
but any type which resolves to one in `self.BASE_TYPES`
may be returned.

*Method `TagsOntology.by_type(self, type_name, with_tagsets=False)`*:
Yield keys or (key,tagset) of type `type_name`
i.e. all keys commencing with *type_name*`.`.

*Method `TagsOntology.convert_tag(self, tag)`*:
Convert a `Tag`'s value accord to the ontology.
Return a new `Tag` with the converted value
or the original `Tag` unchanged.

This is primarily aimed at things like regexp based autotagging,
where the matches are all strings
but various fields have special types,
commonly `int`s or `date`s.

*Method `TagsOntology.edit_indices(self, indices, prefix=None)`*:
Edit the entries specified by indices.
Return `TagSet`s for the entries which were changed.

*Method `TagsOntology.from_match(tagsets, match, unmatch=None)`*:
Initialise a `SubTagSets` from `tagsets`, `match` and optional `unmatch`.

Parameters:
* `tagsets`: a `TagSets` holding ontology information
* `match`: a match function used to choose entries based on a type name
* `unmatch`: an optional reverse for `match`, accepting a subtype
  name and returning its public name

If `match` is `None`
then `tagsets` will always be chosen if no prior entry matched.

Otherwise, `match` is resolved to a function `match-func(type_name)`
which returns a subtype name on a match and a false value on no match.

If `match` is a callable it is used as `match_func` directly.

if `match` is a list, tuple or set
then this method calls itself with `(tagsets,submatch)`
for each member `submatch` if `match`.

If `match` is a `str`,
if it ends in a dot '.', dash '-' or underscore '_'
then it is considered a prefix of `type_name` and the returned
subtype name is the text from `type_name` after the prefix
othwerwise it is considered a full match for the `type_name`
and the returns subtype name is `type_name` unchanged.
The `match` string is a simplistic shell style glob
supporting `*` but not `?` or `[`*seq*`]`.

The value of `unmatch` is constrained by `match`.
If `match` is `None`, `unmatch` must also be `None`;
the type name is used unchanged.
If `match` is callable`, `unmatch` must also be callable;
it is expected to reverse `match`.

Examples:

    >>> from cs.sqltags import SQLTags
    >>> from os.path import expanduser as u
    >>> # an initial empty ontology with a default in memory mapping
    >>> ont = TagsOntology()
    >>> # divert the types actor, role and series to my media ontology
    >>> ont.add_tagsets(
    ...     SQLTags(u('~/var/media-ontology.sqlite')),
    ...     ['actor', 'role', 'series'])
    >>> # divert type "musicbrainz.recording" to mbdb.sqlite
    >>> # mapping to the type "recording"
    >>> ont.add_tagsets(SQLTags(u('~/.cache/mbdb.sqlite')), 'musicbrainz.')
    >>> # divert type "tvdb.actor" to tvdb.sqlite
    >>> # mapping to the type "actor"
    >>> ont.add_tagsets(SQLTags(u('~/.cache/tvdb.sqlite')), 'tvdb.')

*Method `TagsOntology.get(self, name, default=None)`*:
Fetch the entity named `name` or `default`.

*Method `TagsOntology.items(self)`*:
Yield `(entity_name,tags)` for all the items in each subtagsets.

*Method `TagsOntology.keys(self)`*:
Yield entity names for all the entities.

*Method `TagsOntology.metadata(self, type_name, value, *, convert=None)`*:
Return the metadata `TagSet` for `type_name` and `value`.
This implements the mapping between a type's value and its semantics.

The optional parameter `convert`
may specify a function to use to convert `value` to a tag name component
to be used in place of `self.value_to_tag_name` (the default).

For example, if a `TagSet` had a list of characters such as:

    character=["Captain America (Marvel)","Black Widow (Marvel)"]

then these values could be converted to the dotted identifiers
`character.marvel.captain_america`
and `character.marvel.black_widow` respectively,
ready for lookup in the ontology
to obtain the "metadata" `TagSet` for each specific value.

*Method `TagsOntology.startup_shutdown(self)`*:
Open all the sub`TagSets` and close on exit.

*Method `TagsOntology.subtype_name(self, type_name)`*:
Return the type name for use within `self.tagsets` from `type_name`.
Returns `None` if this is not a supported `type_name`.

*Method `TagsOntology.type_name(self, subtype_name)`*:
Return the external type name from the internal `subtype_name`
which is used within `self.tagsets`.

*Method `TagsOntology.type_names(self)`*:
Return defined type names i.e. all entries starting `type.`.

*Method `TagsOntology.type_values(self, type_name, value_tag_name=None)`*:
Yield the various defined values for `type_name`.
This is useful for types with enumerated metadata entries.

For example, if metadata entries exist as `foo.bah` and `foo.baz`
for the `type_name` `'foo'`
then this yields `'bah'` and `'baz'`.`

Note that this looks for a `Tag` for the value,
falling back to the entry suffix if the tag is not present.
That tag is normally named `value`
(from DEFAULT_VALUE_TAG_NAME)
but may be overridden by the `value_tag_name` parameter.
Also note that normally it is desireable that the value
convert to the suffix via the `value_to_tag_name` method
so that the metadata entry can be located from the value.

*Method `TagsOntology.typedef(self, type_name)`*:
Return the `TagSet` defining the type named `type_name`.

*Method `TagsOntology.types(self)`*:
Generator yielding defined type names and their defining `TagSet`.

*Method `TagsOntology.value_to_tag_name(value)`*:
Convert a tag value to a tagnamelike dotted identifierish string
for use in ontology lookup.
Raises `ValueError` for unconvertable values.

We are allowing dashes in the result (UUIDs, MusicBrainz discids, etc).

`int`s are converted to `str`.

Strings are converted as follows:
* a trailing `(.*)` is turned into a prefix with a dot,
  for example `"Captain America (Marvel)"`
  becomes `"Marvel.Captain America"`.
* the string is split into words (nonwhitespace),
  lowercased and joined with underscores,
  for example `"Marvel.Captain America"`
  becomes `"marvel.captain_america"`.

## Class `TagsOntologyCommand(cs.cmdutils.BaseCommand)`

A command line for working with ontology types.

Command line implementation.

Usage summary:

    Usage: tagsontology subcommand [...]
      Subcommands:
        edit [{/name-regexp | entity-name}]
          Edit entities.
          With no arguments, edit all the entities.
          With an argument starting with a slash, edit the entities
          whose names match the regexp.
          Otherwise the argument is expected to be an entity name;
          edit the tags of that entity.
        help [-l] [subcommand-names...]
          Print help for subcommands.
          This outputs the full help for the named subcommands,
          or the short help for all subcommands if no names are specified.
          -l  Long help even if no subcommand-names provided.
        meta tag=value
        shell
          Run a command prompt via cmd.Cmd using this command's subcommands.
        type
            With no arguments, list the defined types.
          type type_name
            With a type name, print its `Tag`s.
          type type_name edit
            Edit the tags defining a type.
          type type_name edit meta_names_pattern...
            Edit the tags for the metadata names matching the
            meta_names_patterns.
          type type_name list
          type type_name ls
            List the metadata names for this type and their tags.
          type type_name + entity_name [tags...]
            Create type_name.entity_name and apply the tags.

*Method `TagsOntologyCommand.cmd_edit(self, argv)`*:
Usage: {cmd} [{{/name-regexp | entity-name}}]
Edit entities.
With no arguments, edit all the entities.
With an argument starting with a slash, edit the entities
whose names match the regexp.
Otherwise the argument is expected to be an entity name;
edit the tags of that entity.

*Method `TagsOntologyCommand.cmd_meta(self, argv)`*:
Usage: {cmd} tag=value

*Method `TagsOntologyCommand.cmd_type(self, argv)`*:
Usage:
{cmd}
  With no arguments, list the defined types.
{cmd} type_name
  With a type name, print its `Tag`s.
{cmd} type_name edit
  Edit the tags defining a type.
{cmd} type_name edit meta_names_pattern...
  Edit the tags for the metadata names matching the
  meta_names_patterns.
{cmd} type_name list
{cmd} type_name ls
  List the metadata names for this type and their tags.
{cmd} type_name + entity_name [tags...]
  Create type_name.entity_name and apply the tags.

# Release Log



*Release 20240422.2*:
jsonable: use obj.for_json() if available.

*Release 20240422.1*:
jsonable: convert pathlib.PurePath to str, hoping this isn't too open ended a can of worms.

*Release 20240422*:
* New jsonable(obj) function to return a deep copy of `obj` which can be transcribed as JSON.
* Tag.transcribe_value: pass jsonable(value) to the JSON encoder, drop special checks now done by jsonable().
* Tag.__str__: do not catch TypeError any more, was embedding Python repr()s in .fstags files - now Tag.transcribe_value() does the correct thing where that is possible.

*Release 20240316*:
Fixed release upload artifacts.

*Release 20240305*:
* Tag.from_str2: make the ontology optional.
* TagSetPrefixView: provide __len__() and update().

*Release 20240211*:
* TagFile.parse_tag_line: recognise dotted_identifiers directly, avoids misparsing bare "nan" as float NaN.
* Tag.parse_value: BUGFIX parse - always to the primary types first (int, float) before trying any funny extra types.

*Release 20240201*:
TagsOntology.metadata: actually call the .items() method!

*Release 20231129*:
* TagSet.__getattr__: rework the attribute lookup with greater precision.
* TagSetPrefixView.__getattr__: if the attribute is not there, raise Attribute error, do not try to fall back to something else.
* TagSet: drop ATTRABLE_MAPPING_DEFAULT=None, caused far more confusion that it was worth.

*Release 20230612*:
* TagFile.save_tagsets: catch and warn about exceptions from update_mapping[key].update, something is wrong with my SQLTags usage.
* TagFile.save_tagsets: update_mapping: do not swallow AttributeError.

*Release 20230407*:
Move the (optional) ORM open/close from FSTags.startup_shutdown to TagFile.save, greatly shortens the ORM lock.

*Release 20230212*:
Mark TagSetCriterion as Promotable.

*Release 20230210*:
* TagFile: new optional update_mapping secondary mapping to which to mirror file tags, for example to an SQLTags.
* New .uuid:UUID property returning the UUID for the tag named 'uuid' or None.

*Release 20230126*:
New TagSet.is_stale() method based on .expiry attribute, intended for TagSets which are caches of other primary data.

*Release 20221228*:
* TagFile: drop _singleton_key, FSPathBasedSingleton provides a good default.
* TagFile.save_tagsets,tags_line: new optional prune=False parameter to drop empty top level dict/lists.
* TagFile.save: plumb prune=False parameter.

*Release 20220806*:
New TagSetCriterion.promote(obj)->TagSetCriterion class method.

*Release 20220606*:
* Tag.parse_value: bugfix parse of float.
* TagSet.edit: accept optional comments parameter with addition header comment lines, be more tolerant of errors, avoid losing data on error.

*Release 20220430*:
* TagSetPrefixView: new as_dict() method.
* TagSetPrefixView.__str__: behave like TagSet.__str__.
* TagFile.save_tagsets: do not try to save if the file is missing and the tagsets are empty.
* New TagSet.from_tags(tags) factory to make a new TagSet from an iterable of tags.
* TagSetPrefixView: add .get and .setdefault mapping methods.
* RegexpTagRule: accept optional tag_prefix parameter.
* Tagset: new from_ini() and save_as_ini() methods to support cs.timeseries config files, probably handy elsewhere.

*Release 20220311*:
Assorted internal changes.

*Release 20211212*:
* Tag: new fallback_parse parameter for value parsing, default get_nonwhite.
* Tag: new from_arg factory with fallback_parse grabbing the whole string for command line arguments, thus supporting unquoted strings for ease of use.
* TagSetCriterion: new optional fallback_parse parameter and from_arg method as for the Tag factories.
* Tag.transcribe_value: accept optional json_options to control the JSON encoder, used for human friendly multiline edits in cs.app.tagger.
* Rename edit_many to edit_tagsets for clarity.
* TagsOntology: new type_values method to return values for a type (derived from their metadata entries).
* Tag: new alt_values method returning its TagsOntology.type_values.
* (Internal) New _FormatStringTagProxy which proxies a Tag but uses str(self.__proxied.value) for __str__ to support format strings.
* (Internal) TagSet.get_value: if arg_name matches a Tag, return a _FormatStringTagProxy.
* Tag.__new__: accept (tag_name,value) or (Tag) as initialisation parameters.

*Release 20210913*:
* TagSet.get_value: raise KeyError in strict mode, leave placeholder otherwise.
* Other small changes.

*Release 20210906*:
Many many updates; some semantics have changed.

*Release 20210428*:
Bugfix TagSet.set: internal in place changes to a complex tag value were not noticed, causing TagFile to not update on shutdown.

*Release 20210420*:
* TagSet: also subclass cs.dateutils.UNIXTimeMixin.
* Various TagSetNamespace updates and bugfixes.

*Release 20210404*:
Bugfix TagBasedTest.COMPARISON_FUNCS["="]: if cmp_value is None, return true (the tag is present).

*Release 20210306*:
* ExtendedNamespace,TagSetNamespace: move the .[:alpha:]* attribute support from ExtendedNamespace to TagSetNamespace because it requires Tags.
* TagSetNamespace.__getattr__: new _i, _s, _f suffixes to return int, str or float tag values (or None); fold _lc in with these.
* Pull most of `TaggedEntity` out into `TaggedEntityMixin` for reuse by domain specific tagged entities.
* TaggedEntity: new .set and .discard methods.
* TaggedEntity: new as_editable_line, from_editable_line, edit and edit_entities methods to support editing entities using a text editor.
* ontologies: type entries are now prefixed with "type." and metadata entries are prefixed with "meta."; provide a worked ontology example in the introduction and improve related docstrings.
* TagsOntology: new .types(), .types_names(), .meta(type_name,value), .meta_names() methods.
* TagsOntology.__getitem__: create missing TagSets on demand.
* New TagsOntologyCommand, initially with a "type [type_name [{edit|list}]]" subcommand, ready for use as the cmd_ont subcommand of other tag related commands.
* TagSet: support initialisation like a dict including keywords, and move the `ontology` parameter to `_onotology`.
* TagSet: include AttrableMappingMixin to enable attribute access to values when there is no conflict with normal methods.
* UUID encode/decode support.
* Honour $TAGSET_EDITOR or $EDITOR as preferred interactive editor for tags.
* New TagSet.subtags(prefix) to extract a subset of the tags.
* TagsOntology.value_metadata: new optional convert parameter to override the default "convert human friendly name" algorithm, particularly to pass convert=str to things which are already the basic id.
* Rename TaggedEntity to TagSet.
* Rename TaggedEntities to TagSets.
* TagSet: new csvrow and from_csvrow methods imported from obsolete TaggedEntityMixin class.
* Move BaseTagFile from cs.fstags to TagFile in cs.tagset.
* TagSet: support access to the tag "c.x" via attributes provided there is no "c" tag in the way.
* TagSet.unixtime: implement the autoset-to-now semantics.
* New as_timestamp(): convert date, datetime, int or float to a UNIX timestamp.
* Assorted docstring updates and bugfixes.

*Release 20200716*:
* Update for changed cs.obj.SingletonMixin API.
* Pull in TaggedEntity from cs.sqltags and add the .csvrow property and the .from_csvrow factory.

*Release 20200521.1*:
Fix DISTINFO.install_requires, drop debug import.

*Release 20200521*:
* New ValueDetail and KeyValueDetail classes for returning ontology information; TagInfo.detail now returns a ValueDetail for scalar types, a list of ValueDetails for sequence types and a list of KeyValueDetails for mapping types; drop various TagInfo mapping/iterable style methods, too confusing to use.
* Plumb ontology parameter throughout, always optional.
* Drop TypedTag, Tags now use ontologies for this.
* New TagsCommandMixin to support BaseCommands which manipulate Tags.
* Many improvements and bugfixes.

*Release 20200318*:
* *Note that the TagsOntology stuff is in flux and totally alpha.*
* Tag.prefix_name factory returning a new tag if prefix is not empty, ptherwise self.
* TagSet.update: accept an optional prefix for inserting "foreign" tags with a distinguishing name prefix.
* Tag.as_json: turn sets and tuples into lists for encoding.
* Backport for Python < 3.7 (no fromisoformat functions).
* TagSet: drop unused and illplaced .titleify, .episode_title and .title methods.
* TagSet: remove "defaults", unused.
* Make TagSet a direct subclass of dict, adjust uses of .update etc.
* New ExtendedNamespace class which is a SimpleNamespace with some inferred attributes and a partial mapping API (keys and __getitem__).
* New TagSet.ns() returning the Tags as an ExtendedNamespace, which doubles as a mapping for str.format_map; TagSet.format_kwargs is now an alias for this.
* New Tag.from_string factory to parse a str into a Tag.
* New TagsOntology and TypedTag classes to provide type and value-detail information; very very alpha and subject to change.

*Release 20200229.1*:
Initial release: pull TagSet, Tag, TagChoice from cs.fstags for independent use.


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "cs.tagset",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": "python3",
    "author": null,
    "author_email": "Cameron Simpson <cs@cskk.id.au>",
    "download_url": "https://files.pythonhosted.org/packages/c2/02/f914b20001439c86522dca27f36ec66195aa4d729f92d5399afc10787479/cs.tagset-20240422.2.tar.gz",
    "platform": null,
    "description": "Tags and sets of tags\nwith __format__ support and optional ontology information.\n\n*Latest release 20240422.2*:\njsonable: use obj.for_json() if available.\n\nSee `cs.fstags` for support for applying these to filesystem objects\nsuch as directories and files.\n\nSee `cs.sqltags` for support for databases of entities with tags,\nnot directly associated with filesystem objects.\nThis is suited to both log entries (entities with no \"name\")\nand large collections of named entities;\nboth accept `Tag`s and can be searched on that basis.\n\nAll of the available complexity is optional:\nyou can use `Tag`s without bothering with `TagSet`s\nor `TagsOntology`s.\n\nThis module contains the following main classes:\n* `Tag`: an object with a `.name` and optional `.value` (default `None`)\n  and also an optional reference `.ontology`\n  for associating semantics with tag values.\n  The `.value`, if not `None`, will often be a string,\n  but may be any Python object.\n  If you're using these via `cs.fstags`,\n  the object will need to be JSON transcribeable.\n* `TagSet`: a `dict` subclass representing a set of `Tag`s\n  to associate with something;\n  it also has setlike `.add` and `.discard` methods.\n  As such it only supports a single `Tag` for a given tag name,\n  but that tag value can of course be a sequence or mapping\n  for more elaborate tag values.\n* `TagsOntology`:\n  a mapping of type names to `TagSet`s defining the type\n  and also to entries for the metadata for specific per-type values.\n\nHere's a simple example with some `Tag`s and a `TagSet`.\n\n    >>> tags = TagSet()\n    >>> # add a \"bare\" Tag named 'blue' with no value\n    >>> tags.add('blue')\n    >>> # add a \"topic=tagging\" Tag\n    >>> tags.set('topic', 'tagging')\n    >>> # make a \"subtopic\" Tag and add it\n    >>> subtopic = Tag('subtopic', 'ontologies')\n    >>> tags.add(subtopic)\n    >>> # Tags have nice repr() and str()\n    >>> subtopic\n    Tag(name='subtopic',value='ontologies')\n    >>> print(subtopic)\n    subtopic=ontologies\n    >>> # a TagSet also has a nice repr() and str()\n    >>> tags\n    TagSet:{'blue': None, 'topic': 'tagging', 'subtopic': 'ontologies'}\n    >>> print(tags)\n    blue subtopic=ontologies topic=tagging\n    >>> tags2 = TagSet({'a': 1}, b=3, c=[1,2,3], d='dee')\n    >>> tags2\n    TagSet:{'a': 1, 'b': 3, 'c': [1, 2, 3], 'd': 'dee'}\n    >>> print(tags2)\n    a=1 b=3 c=[1,2,3] d=dee\n    >>> # since you can print a TagSet to a file as a line of text\n    >>> # you can get it back from a line of text\n    >>> TagSet.from_line('a=1 b=3 c=[1,2,3] d=dee')\n    TagSet:{'a': 1, 'b': 3, 'c': [1, 2, 3], 'd': 'dee'}\n    >>> # because TagSets are dicts you can format strings with them\n    >>> print('topic:{topic} subtopic:{subtopic}'.format_map(tags))\n    topic:tagging subtopic:ontologies\n    >>> # TagSets have convenient membership tests\n    >>> # test for blueness\n    >>> 'blue' in tags\n    True\n    >>> # test for redness\n    >>> 'red' in tags\n    False\n    >>> # test for any \"subtopic\" tag\n    >>> 'subtopic' in tags\n    True\n    >>> # test for subtopic=ontologies\n    >>> print(subtopic)\n    subtopic=ontologies\n    >>> subtopic in tags\n    True\n    >>> # test for subtopic=libraries\n    >>> subtopic2 = Tag('subtopic', 'libraries')\n    >>> subtopic2 in tags\n    False\n\n## Ontologies\n\n`Tag`s and `TagSet`s suffice to apply simple annotations to things.\nHowever, an ontology brings meaning to those annotations.\n\nSee the `TagsOntology` class for implementation details,\naccess methods and more examples.\n\nConsider a record about a movie, with these tags (a `TagSet`):\n\n    title=\"Avengers Assemble\"\n    series=\"Avengers (Marvel)\"\n    cast={\"Scarlett Johansson\":\"Black Widow (Marvel)\"}\n\nwhere we have the movie title,\na name for the series in which it resides,\nand a cast as an association of actors with roles.\n\nAn ontology lets us associate implied types and metadata with these values.\n\nHere's an example ontology supporting the above `TagSet`:\n\n    type.cast type=dict key_type=person member_type=character description=\"members of a production\"\n    type.character description=\"an identified member of a story\"\n    type.series type=str\n    character.marvel.black_widow type=character names=[\"Natasha Romanov\"]\n    person.scarlett_johansson fullname=\"Scarlett Johansson\" bio=\"Known for Black Widow in the Marvel stories.\"\n\nThe type information for a `cast`\nis defined by the ontology entry named `type.cast`,\nwhich tells us that a `cast` `Tag` is a `dict`,\nwhose keys are of type `person`\nand whose values are of type `character`.\n(The default type is `str`.)\n\nTo find out the underlying type for a `character`\nwe look that up in the ontology in turn;\nbecause it does not have a specified `type` `Tag`, it it taken to be a `str`.\n\nHaving the types for a `cast`,\nit is now possible to look up the metadata for the described cast members.\n\nThe key `\"Scarlett Johansson\"` is a `person`\n(from the type definition of `cast`).\nThe ontology entry for her is named `person.scarlett_johansson`\nwhich is computed as:\n* `person`: the type name\n* `scarlett_johansson`: obtained by downcasing `\"Scarlett Johansson\"`\n  and replacing whitespace with an underscore.\n  The full conversion process is defined\n  by the `TagsOntology.value_to_tag_name` function.\n\nThe key `\"Black Widow (Marvel)\"` is a `character`\n(again, from the type definition of `cast`).\nThe ontology entry for her is named `character.marvel.black_widow`\nwhich is computed as:\n* `character`: the type name\n* `marvel.black_widow`: obtained by downcasing `\"Black Widow (Marvel)\"`,\n  replacing whitespace with an underscore,\n  and moving a bracketed suffix to the front as an unbracketed prefix.\n  The full conversion process is defined\n  by the `TagsOntology.value_to_tag_name` function.\n\n## Format Strings\n\nYou can just use `str.format_map` as shown above\nfor the direct values in a `TagSet`,\nsince it subclasses `dict`.\n\nHowever, `TagSet`s also subclass `cs.lex.FormatableMixin`\nand therefore have a richer `format_as` method which has an extended syntax\nfor the format component.\nCommand line tools like `fstags` use this for output format specifications.\n\nAn example:\n\n    >>> # an ontology specifying the type for a colour\n    >>> # and some information about the colour \"blue\"\n    >>> ont = TagsOntology(\n    ...   {\n    ...       'type.colour':\n    ...       TagSet(description=\"a colour, a hue\", type=\"str\"),\n    ...       'colour.blue':\n    ...       TagSet(\n    ...           url='https://en.wikipedia.org/wiki/Blue',\n    ...           wavelengths='450nm-495nm'\n    ...       ),\n    ...   }\n    ... )\n    >>> # tag set with a \"blue\" tag, using the ontology above\n    >>> tags = TagSet(colour='blue', labels=['a', 'b', 'c'], size=9, _ontology=ont)\n    >>> tags.format_as('The colour is {colour}.')\n    'The colour is blue.'\n    >>> # format a string about the tags showing some metadata about the colour\n    >>> tags.format_as('Information about the colour may be found here: {colour:metadata.url}')\n    'Information about the colour may be found here: https://en.wikipedia.org/wiki/Blue'\n\n## Function `as_unixtime(tag_value)`\n\nConvert a tag value to a UNIX timestamp.\n\nThis accepts `int`, `float` (already a timestamp)\nand `date` or `datetime`\n(use `datetime.timestamp() for a nonnaive `datetime`,\notherwise `time.mktime(tag_value.time_tuple())`,\nwhich assumes the local time zone).\n\n## Class `BaseTagSets(cs.resources.MultiOpenMixin, collections.abc.MutableMapping)`\n\nBase class for collections of `TagSet` instances\nsuch as `cs.fstags.FSTags` and `cs.sqltags.SQLTags`.\n\nExamples of this include:\n* `cs.cdrip.MBSQLTags`: a mapping of MusicbrainsNG entities to their associated `TagSet`\n* `cs.fstags.FSTags`: a mapping of filesystem paths to their associated `TagSet`\n* `cs.sqltags.SQLTags`: a mapping of names to `TagSet`s stored in an SQL database\n\nSubclasses must implement:\n* `get(name,default=None)`: return the `TagSet` associated\n  with `name`, or `default`.\n* `__setitem__(name,tagset)`: associate a `TagSet`with the key `name`;\n  this is called by the `__missing__` method with a newly created `TagSet`.\n* `keys(self)`: return an iterable of names\n\nSubclasses may reasonably want to override the following:\n* `startup_shutdown(self)`: context manager to allocate and release any\n  needed resources such as database connections\n\nSubclasses may implement:\n* `__len__(self)`: return the number of names\n\nThe `TagSet` factory used to fetch or create a `TagSet` is\nnamed `TagSetClass`. The default implementation honours two\nclass attributes:\n* `TAGSETCLASS_DEFAULT`: initially `TagSet`\n* `TAGSETCLASS_PREFIX_MAPPING`: a mapping of type names to `TagSet` subclasses\n\nThe type name of a `TagSet` name is the first dotted component.\nFor example, `artist.nick_cave` has the type name `artist`.\nA subclass of `BaseTagSets` could utiliise an `ArtistTagSet` subclass of `TagSet`\nand provide:\n\n    TAGSETCLASS_PREFIX_MAPPING = {\n      'artist': ArtistTagSet,\n    }\n\nin its class definition. Accesses to `artist.`* entities would\nresult in `ArtistTagSet` instances and access to other enitities\nwould result in ordinary `TagSet` instances.\n\n*Method `BaseTagSets.__init__(self, *, ontology=None)`*:\nInitialise the collection.\n\n*`BaseTagSets.TAGSETCLASS_DEFAULT`*\n\n*Method `BaseTagSets.TagSetClass(self, *, name, **kw)`*:\nFactory to create a new `TagSet` from `name`.\n\n*Method `BaseTagSets.__contains__(self, name: str)`*:\nTest whether `name` is present in the underlying mapping.\n\n*Method `BaseTagSets.__getitem__(self, name: str)`*:\nObtain the `TagSet` associated with `name`.\n\nIf `name` is not presently mapped,\nreturn `self.__missing__(name)`.\n\n*Method `BaseTagSets.__iter__(self)`*:\nIteration returns the keys.\n\n*Method `BaseTagSets.__len__(self)`*:\nReturn the length of the underlying mapping.\n\n*Method `BaseTagSets.__missing__(self, name: str, **kw)`*:\nLike `dict`, the `__missing__` method may autocreate a new `TagSet`.\n\nThis is called from `__getitem__` if `name` is missing\nand uses the factory `cls.default_factory`.\nIf that is `None` raise `KeyError`,\notherwise call `self.default_factory(name,**kw)`.\nIf that returns `None` raise `KeyError`,\notherwise save the entity under `name` and return the entity.\n\n*Method `BaseTagSets.__setitem__(self, name, te)`*:\nSave `te` in the backend under the key `name`.\n\n*Method `BaseTagSets.add(self, name: str, **kw)`*:\nReturn a new `TagSet` associated with `name`,\nwhich should not already be in use.\n\n*Method `BaseTagSets.default_factory(self, name: str)`*:\nCreate a new `TagSet` named `name`.\n\n*Method `BaseTagSets.edit(self, *, select_tagset=None, **kw)`*:\nEdit the `TagSet`s.\n\nParameters:\n* `select_tagset`: optional callable accepting a `TagSet`\n  which tests whether it should be included in the `TagSet`s\n  to be edited\nOther keyword arguments are passed to `Tag.edit_tagsets`.\n\n*Method `BaseTagSets.get(self, name: str, default=None)`*:\nReturn the `TagSet` associated with `name`,\nor `default` if there is no such entity.\n\n*Method `BaseTagSets.items(self, *, prefix=None)`*:\nGenerator yielding `(key,value)` pairs,\noptionally constrained to keys starting with `prefix+'.'`.\n\n*Method `BaseTagSets.keys(self, *, prefix=None)`*:\nReturn the keys starting with `prefix+'.'`\nor all keys if `prefix` is `None`.\n\n*Method `BaseTagSets.subdomain(self, subname: str)`*:\nReturn a proxy for this `BaseTagSets` for the `name`s\nstarting with `subname+'.'`.\n\n*Method `BaseTagSets.values(self, *, prefix=None)`*:\nGenerator yielding the mapping values (`TagSet`s),\noptionally constrained to keys starting with `prefix+'.'`.\n\n## Function `jsonable(obj, converted: dict)`\n\nConvert `obj` to a JSON encodable form.\nThis returns `obj` for purely JSONable objects and a JSONable\ndeep copy of `obj` if it or some subcomponent required\nconversion.\n`converted` is a dict mapping object ids to their converted forms\nto prevent loops.\n\n## Class `MappingTagSets(BaseTagSets)`\n\nA `BaseTagSets` subclass using an arbitrary mapping.\n\nIf no mapping is supplied, a `dict` is created for the purpose.\n\nExample:\n\n    >>> tagsets = MappingTagSets()\n    >>> list(tagsets.keys())\n    []\n    >>> tagsets.get('foo')\n    >>> tagsets['foo'] = TagSet(bah=1, zot=2)\n    >>> list(tagsets.keys())\n    ['foo']\n    >>> tagsets.get('foo')\n    TagSet:{'bah': 1, 'zot': 2}\n    >>> list(tagsets.keys(prefix='foo'))\n    ['foo']\n    >>> list(tagsets.keys(prefix='bah'))\n    []\n\n*Method `MappingTagSets.__delitem__(self, name)`*:\nDelete the `TagSet` named `name`.\n\n*Method `MappingTagSets.__setitem__(self, name, te)`*:\nSave `te` in the backend under the key `name`.\n\n*Method `MappingTagSets.keys(self, *, prefix: Optional[str] = None)`*:\nReturn an iterable of the keys commencing with `prefix`\nor all keys if `prefix` is `None`.\n\n## Class `RegexpTagRule`\n\nA regular expression based `Tag` rule.\n\nThis applies a regular expression to a string\nand returns inferred `Tag`s.\n\n*Method `RegexpTagRule.infer_tags(self, s)`*:\nApply the rule to the string `s`, return a list of `Tag`s.\n\n## Function `selftest(argv)`\n\nRun some ad hoc self tests.\n\n## Class `Tag(Tag, cs.lex.FormatableMixin)`\n\nA `Tag` has a `.name` (`str`) and a `.value`\nand an optional `.ontology`.\n\nThe `name` must be a dotted identifier.\n\nTerminology:\n* A \"bare\" `Tag` has a `value` of `None`.\n* A \"naive\" `Tag` has an `ontology` of `None`.\n\nThe constructor for a `Tag` is unusual:\n* both the `value` and `ontology` are optional,\n  defaulting to `None`\n* if `name` is a `str` then we always construct a new `Tag`\n  with the suppplied values\n* if `name` is not a `str`\n  it should be a `Tag`like object to promote;\n  it is an error if the `value` parameter is not `None`\n  in this case\n* an optional `prefix` may be supplied\n  which is prepended to `name` with a dot (`'.'`) if not empty\n\nThe promotion process is as follows:\n* if `name` is a `Tag` subinstance\n  then if the supplied `ontology` is not `None`\n  and is not the ontology associated with `name`\n  then a new `Tag` is made,\n  otherwise the original `Tag` is returned unchanged\n* otherwise a new `Tag` is made from `name`\n  using its `.value`\n  and overriding its `.ontology`\n  if the `ontology` parameter is not `None`\n\nExamples:\n\n    >>> ont = TagsOntology({'colour.blue': TagSet(wavelengths='450nm-495nm')})\n    >>> tag0 = Tag('colour', 'blue')\n    >>> tag0\n    Tag(name='colour',value='blue')\n    >>> tag = Tag(tag0)\n    >>> tag\n    Tag(name='colour',value='blue')\n    >>> tag = Tag(tag0, ontology=ont)\n    >>> tag # doctest: +ELLIPSIS\n    Tag(name='colour',value='blue',ontology=...)\n    >>> tag = Tag(tag0, prefix='surface')\n    >>> tag\n    Tag(name='surface.colour',value='blue')\n\n*Method `Tag.__init__(self, *a, **kw)`*:\nDummy `__init__` to avoid `FormatableMixin.__init__`\nbecause we subclass `namedtuple` which has no `__init__`.\n\n*`Tag.__hash__`*\n\n*Method `Tag.__str__(self)`*:\nEncode `name` and `value`.\n\n*Method `Tag.alt_values(self, value_tag_name=None)`*:\nReturn a list of alternative values for this `Tag`\non the premise that each has a metadata entry.\n\n*Property `Tag.basetype`*:\nThe base type name for this tag.\nReturns `None` if there is no ontology.\n\nThis calls `self.onotology.basetype(self.name)`.\nThe basetype is the endpoint of a cascade down the defined types.\n\nFor example, this might tell us that a `Tag` `role=\"Fred\"`\nhas a basetype `\"str\"`\nby cascading through a hypothetical chain `role`->`character`->`str`:\n\n    type.role type=character\n    type.character type=str\n\n*Method `Tag.from_arg(arg, offset=0, ontology=None)`*:\nParse a `Tag` from the string `arg` at `offset` (default `0`).\nwhere `arg` is known to be entirely composed of the value,\nsuch as a command line argument.\n\nThis calls the `from_str` method with `fallback_parse` set\nto gather then entire tail of the supplied string `arg`.\n\n*Method `Tag.from_str(s, offset=0, ontology=None, fallback_parse=None)`*:\nParse a `Tag` definition from `s` at `offset` (default `0`).\n\n*Method `Tag.from_str2(s, offset=0, *, ontology=None, extra_types=None, fallback_parse=None)`*:\nParse tag_name[=value], return `(Tag,offset)`.\n\n*Method `Tag.is_valid_name(name)`*:\nTest whether a tag name is valid: a dotted identifier.\n\n*Method `Tag.key_metadata(self, key)`*:\nReturn the metadata definition for `key`.\n\nThe metadata `TagSet` is obtained from the ontology entry\n*type*`.`*key_tag_name*\nwhere *type* is the `Tag`'s `key_type`\nand *key_tag_name* is the key converted\ninto a dotted identifier by `TagsOntology.value_to_tag_name`.\n\n*Property `Tag.key_type`*:\nThe type name for members of this tag.\n\nThis is required if `.value` is a mapping.\n\n*Property `Tag.key_typedef`*:\nThe typedata definition for this `Tag`'s keys.\n\nThis is for `Tag`s which store mappings,\nfor example a movie cast, mapping actors to roles.\n\nThe name of the member type comes from\nthe `key_type` entry from `self.typedata`.\nThat name is then looked up in the ontology's types.\n\n*Method `Tag.matches(self, tag_name, value)`*:\nTest whether this `Tag` matches `(tag_name,value)`.\n\n*Method `Tag.member_metadata(self, member_key)`*:\nReturn the metadata definition for self[member_key].\n\nThe metadata `TagSet` is obtained from the ontology entry\n*type*`.`*member_tag_name*\nwhere *type* is the `Tag`'s `member_type`\nand *member_tag_name* is the member value converted\ninto a dotted identifier by `TagsOntology.value_to_tag_name`.\n\n*Property `Tag.member_type`*:\nThe type name for members of this tag.\n\nThis is required if `.value` is a sequence or mapping.\n\n*Property `Tag.member_typedef`*:\nThe typedata definition for this `Tag`'s members.\n\nThis is for `Tag`s which store mappings or sequences,\nfor example a movie cast, mapping actors to roles,\nor a list of scenes.\n\nThe name of the member type comes from\nthe `member_type` entry from `self.typedata`.\nThat name is then looked up in the ontology's types.\n\n*Property `Tag.meta`*:\nShortcut property for the metadata `TagSet`.\n\n*Method `Tag.metadata(self, *, ontology=None, convert=None)`*:\nFetch the metadata information about this specific tag value,\nderived through the `ontology` from the tag name and value.\nThe default `ontology` is `self.ontology`.\n\nFor a scalar type (`int`, `float`, `str`) this is the ontology `TagSet`\nfor `self.value`.\n\nFor a sequence (`list`) this is a list of the metadata\nfor each member.\n\nFor a mapping (`dict`) this is mapping of `key->metadata`.\n\n*Method `Tag.parse_name(s, offset=0)`*:\nParse a tag name from `s` at `offset`: a dotted identifier.\n\n*Method `Tag.parse_value(s, offset=0, *, extra_types=None, fallback_parse=None)`*:\nParse a value from `s` at `offset` (default `0`).\nReturn the value, or `None` on no data.\n\nThe optional `extra_types` parameter may be an iterable of\n`(type,from_str,to_str)` tuples where `from_str` is a\nfunction which takes a string and returns a Python object\n(expected to be an instance of `type`).\nThe default comes from `cls.EXTRA_TYPES`.\nThis supports storage of nonJSONable values in text form.\n\nThe optional `fallback_parse` parameter\nspecifies a parse function accepting `(s,offset)`\nand returning `(parsed,new_offset)`\nwhere `parsed` is text from `s[offset:]`\nand `new_offset` is where the parse stopped.\nThe default is `cs.lex.get_nonwhite`\nto gather nonwhitespace characters,\nintended to support *tag_name*`=`*bare_word*\nin human edited tag files.\n\nThe core syntax for values is JSON;\nvalue text commencing with any of `'\"'`, `'['` or `'{'`\nis treated as JSON and decoded directly,\nleaving the offset at the end of the JSON parse.\n\nOtherwise all the nonwhitespace at this point is collected\nas the value text,\nleaving the offset at the next whitespace character\nor the end of the string.\nThe text so collected is then tried against the `from_str`\nfunction of each `extra_types`;\nthe first successful parse is accepted as the value.\nIf no extra type match,\nthe text is tried against `int()` and `float()`;\nif one of these parses the text and `str()` of the result round trips\nto the original text\nthen that value is used.\nOtherwise the text itself is kept as the value.\n\n*Method `Tag.transcribe_value(value, extra_types=None, json_options=None)`*:\nTranscribe `value` for use in `Tag` transcription.\n\nThe optional `extra_types` parameter may be an iterable of\n`(type,from_str,to_str)` tuples where `to_str` is a\nfunction which takes a string and returns a Python object\n(expected to be an instance of `type`).\nThe default comes from `cls.EXTRA_TYPES`.\n\nIf `value` is an instance of `type`\nthen the `to_str` function is used to transcribe the value\nas a `str`, which should not include any whitespace\n(because of the implementation of `parse_value`).\nIf there is no matching `to_str` function,\n`cls.JSON_ENCODER.encode` is used to transcribe `value`.\n\nThis supports storage of nonJSONable values in text form.\n\n*Property `Tag.typedef`*:\nThe defining `TagSet` for this tag's name.\n\nThis is how its type is defined,\nand is obtained from:\n`self.ontology['type.'+self.name]`\n\nBasic `Tag`s often do not need a type definition;\nthese are only needed for structured tag values\n(example: a mapping of cast members)\nor when a `Tag` name is an alias for another type\n(example: a cast member name might be an `actor`\nwhich in turn might be a `person`).\n\nFor example, a `Tag` `colour=blue`\ngets its type information from the `type.colour` entry in an ontology;\nthat entry is just a `TagSet` with relevant information.\n\n## Function `tag_or_tag_value(*da, **dkw)`\n\nA decorator for functions or methods which may be called as:\n\n    func(name[,value])\n\nor as:\n\n    func(Tag)\n\nThe optional decorator argument `no_self` (default `False`)\nshould be supplied for plain functions\nas they have no leading `self` parameter to accomodate.\n\nExample:\n\n    @tag_or_tag_value\n    def add(self, tag_name, value, *, verbose=None):\n\nThis defines a `.add()` method\nwhich can be called with `name` and `value`\nor with single `Tag`like object\n(something with `.name` and `.value` attributes),\nfor example:\n\n    tags = TagSet()\n    ....\n    tags.add('colour', 'blue')\n    ....\n    tag = Tag('size', 9)\n    tags.add(tag)\n\n## Class `TagBasedTest(TagBasedTest, TagSetCriterion)`\n\nA test based on a `Tag`.\n\nAttributes:\n* `spec`: the source text from which this choice was parsed,\n  possibly `None`\n* `choice`: the apply/reject flag\n* `tag`: the `Tag` representing the criterion\n* `comparison`: an indication of the test comparison\n\nThe following comparison values are recognised:\n* `None`: test for the presence of the `Tag`\n* `'='`: test that the tag value equals `tag.value`\n* `'<'`: test that the tag value is less than `tag.value`\n* `'<='`: test that the tag value is less than or equal to `tag.value`\n* `'>'`: test that the tag value is greater than `tag.value`\n* `'>='`: test that the tag value is greater than or equal to `tag.value`\n* `'~/'`: test if the tag value as a regexp is present in `tag.value`\n* '~': test if a matching tag value is present in `tag.value`\n\n*Method `TagBasedTest.by_tag_value(tag_name, tag_value, *, choice=True, comparison='=')`*:\nReturn a `TagBasedTest` based on a `Tag` or `tag_name,tag_value`.\n\n*Method `TagBasedTest.match_tagged_entity(self, te: 'TagSet') -> bool`*:\nTest against the `Tag`s in `tags`.\n\n*Note*: comparisons when `self.tag.name` is not in `tags`\nalways return `False` (possibly inverted by `self.choice`).\n\n*Method `TagBasedTest.parse(s, offset=0, delim=None)`*:\nParse *tag_name*[{`<`|`<=`|'='|'>='|`>`|'~'}*value*]\nand return `(dict,offset)`\nwhere the `dict` contains the following keys and values:\n* `tag`: a `Tag` embodying the tag name and value\n* `comparison`: an indication of the test comparison\n\n## Class `TagFile(cs.fs.FSPathBasedSingleton, BaseTagSets)`\n\nA reference to a specific file containing tags.\n\nThis manages a mapping of `name` => `TagSet`,\nitself a mapping of tag name => tag value.\n\n*Method `TagFile.__setitem__(self, name, te)`*:\nSet item `name` to `te`.\n\n*Method `TagFile.get(self, name, default=None)`*:\nGet from the tagsets.\n\n*Method `TagFile.is_modified(self)`*:\nTest whether this `TagSet` has been modified.\n\n*Method `TagFile.keys(self, *, prefix=None)`*:\n`tagsets.keys`\n\nIf the options `prefix` is supplied,\nyield only those keys starting with `prefix`.\n\n*Method `TagFile.load_tagsets(filepath, ontology, extra_types=None)`*:\nLoad `filepath` and return `(tagsets,unparsed)`.\n\nThe returned `tagsets` are a mapping of `name`=>`tag_name`=>`value`.\nThe returned `unparsed` is a list of `(lineno,line)`\nfor lines which failed the parse (excluding the trailing newline).\n\n*Property `TagFile.names`*:\nThe names from this `FSTagsTagFile` as a list.\n\n*Method `TagFile.parse_tags_line(line, ontology=None, verbose=None, extra_types=None) -> Tuple[str, cs.tagset.TagSet]`*:\nParse a \"name tags...\" line as from a `.fstags` file,\nreturn `(name,TagSet)`.\n\n*Method `TagFile.save(self, extra_types=None, prune=False)`*:\nSave the tag map to the tag file if modified.\n\n*Method `TagFile.save_tagsets(filepath, tagsets, unparsed, extra_types=None, prune=False, update_mapping: Optional[Mapping] = None, update_prefix: Optional[str] = None, update_uuid_tag_name: Optional[str] = None)`*:\nSave `tagsets` and `unparsed` to `filepath`.\n\nThis method will create the required intermediate directories\nif missing.\n\nThis method *does not* clear the `.modified` attribute of the `TagSet`s\nbecause it does not know it is saving to the `Tagset`'s primary location.\n\n*Method `TagFile.startup_shutdown(self)`*:\nSave the tagsets if modified.\n\n*Method `TagFile.tags_line(name, tags, extra_types=None, prune=False)`*:\nTranscribe a `name` and its `tags` for use as a `.fstags` file line.\n\n*Property `TagFile.tagsets`*:\nThe tag map from the tag file,\na mapping of name=>`TagSet`.\n\nThis is loaded on demand.\n\n*Method `TagFile.update(self, name, tags, *, prefix=None, verbose=None)`*:\nUpdate the tags for `name` from the supplied `tags`\nas for `Tagset.update`.\n\n## Class `TagsCommandMixin`\n\nUtility methods for `cs.cmdutils.BaseCommand` classes working with tags.\n\nOptional subclass attributes:\n* `TAGSET_CRITERION_CLASS`: a `TagSetCriterion` duck class,\n  default `TagSetCriterion`.\n  For example, `cs.sqltags` has a subclass\n  with an `.extend_query` method for computing an SQL JOIN\n  used in searching for tagged entities.\n\n*`TagsCommandMixin.TagAddRemove`*\n\n*Method `TagsCommandMixin.parse_tag_addremove(arg, offset=0)`*:\nParse `arg` as an add/remove tag specification\nof the form [`-`]*tag_name*[`=`*value*].\nReturn `(remove,Tag)`.\n\nExamples:\n\n    >>> TagsCommandMixin.parse_tag_addremove('a')\n    TagAddRemove(remove=False, tag=Tag(name='a',value=None))\n    >>> TagsCommandMixin.parse_tag_addremove('-a')\n    TagAddRemove(remove=True, tag=Tag(name='a',value=None))\n    >>> TagsCommandMixin.parse_tag_addremove('a=1')\n    TagAddRemove(remove=False, tag=Tag(name='a',value=1))\n    >>> TagsCommandMixin.parse_tag_addremove('-a=1')\n    TagAddRemove(remove=True, tag=Tag(name='a',value=1))\n    >>> TagsCommandMixin.parse_tag_addremove('-a=\"foo bah\"')\n    TagAddRemove(remove=True, tag=Tag(name='a',value='foo bah'))\n    >>> TagsCommandMixin.parse_tag_addremove('-a=foo bah')\n    TagAddRemove(remove=True, tag=Tag(name='a',value='foo bah'))\n\n*Method `TagsCommandMixin.parse_tag_choices(argv)`*:\nParse `argv` as an iterable of [`!`]*tag_name*[`=`*tag_value`] `Tag`\nadditions/deletions.\n\n*Method `TagsCommandMixin.parse_tagset_criteria(argv, tag_based_test_class=None)`*:\nParse tag specifications from `argv` until an unparseable item is found.\nReturn `(criteria,argv)`\nwhere `criteria` is a list of the parsed criteria\nand `argv` is the remaining unparsed items.\n\nEach item is parsed via\n`cls.parse_tagset_criterion(item,tag_based_test_class)`.\n\n*Method `TagsCommandMixin.parse_tagset_criterion(arg, tag_based_test_class=None)`*:\nParse `arg` as a tag specification\nand return a `tag_based_test_class` instance\nvia its `.from_str` factory method.\nRaises `ValueError` in a misparse.\nThe default `tag_based_test_class`\ncomes from `cls.TAGSET_CRITERION_CLASS`,\nwhich itself defaults to class `TagSetCriterion`.\n\nThe default `TagSetCriterion.from_str` recognises:\n* `-`*tag_name*: a negative requirement for *tag_name*\n* *tag_name*[`=`*value*]: a positive requirement for a *tag_name*\n  with optional *value*.\n\n## Class `TagSet(builtins.dict, cs.dateutils.UNIXTimeMixin, cs.lex.FormatableMixin, cs.mappings.AttrableMappingMixin)`\n\nA setlike class associating a set of tag names with values.\n\nThis actually subclasses `dict`, so a `TagSet` is a direct\nmapping of tag names to values.\nIt accepts attribute access to simple tag values when they\ndo not conflict with the class methods;\nthe reliable method is normal item access.\n\n*NOTE*: iteration yields `Tag`s, not dict keys.\n\nAlso note that all the `Tags` from a `TagSet`\nshare its ontology.\n\nSubclasses should override the `set` and `discard` methods;\nthe `dict` and mapping methods\nare defined in terms of these two basic operations.\n\n`TagSet`s have a few special properties:\n* `id`: a domain specific identifier;\n  this may reasonably be `None` for entities\n  not associated with database rows;\n  the `cs.sqltags.SQLTags` class associates this\n  with the database row id.\n* `name`: the entity's name;\n  a read only alias for the `'name'` `Tag`.\n  The `cs.sqltags.SQLTags` class defines \"log entries\"\n  as `TagSet`s with no `name`.\n* `unixtime`: a UNIX timestamp,\n  a `float` holding seconds since the UNIX epoch\n  (midnight, 1 January 1970 UTC).\n  This is typically the row creation time\n  for entities associated with database rows,\n  but usually the event time for `TagSet`s describing an event.\n\nBecause ` TagSet` subclasses `cs.mappings.AttrableMappingMixin`\nyou can also access tag values as attributes\n*provided* that they do not conflict with instance attributes\nor class methods or properties.\n\n*Method `TagSet.__init__(self, *a, _id=None, _ontology=None, **kw)`*:\nInitialise the `TagSet`.\n\nParameters:\n* positional parameters initialise the `dict`\n  and are passed to `dict.__init__`\n* `_id`: optional identity value for databaselike implementations\n* `_ontology`: optional `TagsOntology to use for this `TagSet`\n* other alphabetic keyword parameters are also used to initialise the\n  `dict` and are passed to `dict.__init__`\n\n*Method `TagSet.__contains__(self, tag)`*:\nTest for a tag being in this `TagSet`.\n\nIf the supplied `tag` is a `str` then this test\nis for the presence of `tag` in the keys.\n\nOtherwise,\nfor each tag `T` in the tagset\ntest `T.matches(tag)` and return `True` on success.\nThe default `Tag.matches` method compares the tag name\nand if the same,\nreturns true if `tag.value` is `None` (basic \"is the tag present\" test)\nand otherwise true if `tag.value==T.value` (basic \"tag value equality\" test).\n\nOtherwise return `False`.\n\n*Method `TagSet.__getattr__(self, attr)`*:\nSupport access to dotted name attributes.\n\nThe following attribute accesses are supported:\n- an attrbute from a superclass\n- a `Tag` whose name is `attr`; return its value\n- the value of `self.auto_infer(attr)` if that does not raise `ValueError`\n- if `self.ontology`, try {type}_{field} and {type}_{field}s\n- otherwise return `self.subtags(attr)` to allow access to dotted tags,\n  provided any existing tags start with \"attr.\"\n\nIf this `TagSet` has an ontology\nand `attr looks like *typename*`_`*fieldname*\nand *typename* is a key,\nlook up the metadata for the `Tag` value\nand return the metadata's *fieldname* key.\nThis also works for plural values.\n\nFor example if a `TagSet` has the tag `artists=[\"fred\",\"joe\"]`\nand `attr` is `artist_names`\nthen the metadata entries for `\"fred\"` and `\"joe\"` are looked up\nand their `artist_name` tags are returned,\nperhaps resulting in the list\n`[\"Fred Thing\",\"Joe Thang\"]`.\n\nIf there are keys commencing with `attr+'.'`\nthen this returns a view of those keys\nso that a subsequent attribute access can access one of those keys.\n\nOtherwise, a superclass attribute access is performed.\n\nExample of dotted access to tags like `c.x`:\n\n    >>> tags=TagSet(a=1,b=2)\n    >>> tags.a\n    1\n    >>> tags.c\n    Traceback (most recent call last):\n        ...\n    AttributeError: TagSet.c\n    >>> tags['c.z']=9\n    >>> tags['c.x']=8\n    >>> tags\n    TagSet:{'a': 1, 'b': 2, 'c.z': 9, 'c.x': 8}\n    >>> tags.c\n    TagSetPrefixView:c.{'z': 9, 'x': 8}\n    >>> tags.c.z\n    9\n\nHowever, this is not supported when there is a tag named `'c'`\nbecause `tags.c` has to return the `'c'` tag value:\n\n    >>> tags=TagSet(a=1,b=2,c=3)\n    >>> tags.a\n    1\n    >>> tags.c\n    3\n    >>> tags['c.z']=9\n    >>> tags.c.z\n    Traceback (most recent call last):\n      File \"<stdin>\", line 1, in <module>\n    AttributeError: 'int' object has no attribute 'z'\n\n*Method `TagSet.__iter__(self, prefix=None, ontology=None)`*:\nYield the tag data as `Tag`s.\n\n*Method `TagSet.__setattr__(self, attr, value)`*:\nAttribute based `Tag` access.\n\nIf `attr` is private or is in `self.__dict__` then that is updated,\nsupporting \"normal\" attributes set on the instance.\nOtherwise the `Tag` named `attr` is set to `value`.\n\nThe `__init__` methods of subclasses should do something like this\n(from `TagSet.__init__`)\nto set up the ordinary instance attributes\nwhich are not to be treated as `Tag`s:\n\n    self.__dict__.update(id=_id, ontology=_ontology, modified=False)\n\n*Method `TagSet.__str__(self)`*:\nThe `TagSet` suitable for writing to a tag file.\n\n*Method `TagSet.add(self, tag_name, value, **kw)`*:\nAdding a `Tag` calls the class `set()` method.\n\n*Method `TagSet.as_dict(self)`*:\nReturn a `dict` mapping tag name to value.\n\n*Method `TagSet.as_tags(self, prefix=None, ontology=None)`*:\nYield the tag data as `Tag`s.\n\n*Property `TagSet.auto`*:\nThe automatic namespace.\nHere we can refer to dotted tag names directly as attributes.\n\n*Method `TagSet.auto_infer(self, attr)`*:\nThe default inference implementation.\n\nThis should return a value if `attr` is inferrable\nand raise `ValueError` if not.\n\nThe default implementation returns the direct tag value for `attr`\nif present.\n\n*Property `TagSet.csvrow`*:\nThis `TagSet` as a list useful to a `csv.writer`.\nThe inverse of `from_csvrow`.\n\n*Method `TagSet.discard(self, tag_name, value, *, verbose=None)`*:\nDiscard the tag matching `(tag_name,value)`.\nReturn a `Tag` with the old value,\nor `None` if there was no matching tag.\n\nNote that if the tag value is `None`\nthen the tag is unconditionally discarded.\nOtherwise the tag is only discarded\nif its value matches.\n\n*Method `TagSet.dump(self, keys=None, *, preindent=None, file=None, **pf_kwargs)`*:\nDump a `TagSet` in multiline format.\n\nParameters:\n* `keys`: optional iterable of `Tag` names to print\n* `file`: optional keyword parameter specifying the output filelike \n  object; the default is `sys.stdout`.\n* `preindent`: optional leading indentation for the entire dump,\n  either a `str` or an `int` indicating a number of spaces\nOther keyword arguments are passed to `pprint.pformat`.\n\n*Method `TagSet.edit(self, editor=None, verbose=None, comments=())`*:\nEdit this `TagSet`.\n\n*Method `TagSet.edit_tagsets(tes, editor=None, verbose=True)`*:\nEdit a collection of `TagSet`s.\nReturn a list of `(old_name,new_name,TagSet)` for those which were modified.\n\nThis function supports modifying both `name` and `Tag`s.\nThe `Tag`s are updated directly.\nThe changed names are returning in the `old_name,new_name` above.\n\nThe collection `tes` may be either a mapping of name/key\nto `TagSet` or an iterable of `TagSets`. If the latter, a\nmapping is made based on `te.name or te.id` for each item\n`te` in the iterable.\n\n*Method `TagSet.from_csvrow(csvrow)`*:\nConstruct a `TagSet` from a CSV row like that from\n`TagSet.csvrow`, being `unixtime,id,name,tags...`.\n\n*Method `TagSet.from_ini(f, section: str, missing_ok=False)`*:\nLoad a `TagSet` from a section of a `.ini` file.\n\nParameters:\n* `f`: the `.ini` format file to read;\n  an iterable of lines (eg a file object)\n  or the name of a file to open\n* `section`: the name of the config section\n  from which to load the `TagSet`\n* `missing_ok`: optional flag, default `False`;\n  if true a missing file will return an empty `TagSet`\n  instead of raising `FileNotFoundError`\n\n*Method `TagSet.from_line(line, offset=0, *, ontology=None, extra_types=None, verbose=None)`*:\nCreate a new `TagSet` from a line of text.\n\n*Method `TagSet.from_tags(tags, _id=None, _ontology=None)`*:\nMake a `TagSet` from an iterable of `Tag`s.\n\n*Method `TagSet.get_arg_name(self, field_name)`*:\nOverride for `FormattableMixin.get_arg_name`:\nreturn the leading dotted identifier,\nwhich represents a tag or tag prefix.\n\n*Method `TagSet.get_value(self, arg_name, a, kw)`*:\nOverride for `FormattableMixin.get_value`:\nlook up `arg_name` in `kw`, return a value.\n\nThe value is obtained as follows:\n* `kw[arg_name]`: the `Tag` named `arg_name` if present\n* `kw.get_format_attribute(arg_name)`:\n  a formattable attribute named `arg_name`\notherwise raise `KeyError` if `self.format_mode.strict`\notherwise return the placeholder string `'{'+arg_name+'}'`.\n\n*Method `TagSet.is_stale(self, max_age=None)`*:\nTest whether this `TagSet` is stale\ni.e. the time since `self.last_updated` UNIX time exceeds `max_age` seconds\n(default from `self.STALE_AGE`).\n\nThis is a convenience function for `TagSet`s which cache external data.\n\n*Property `TagSet.name`*:\nRead only `name` property, `None` if there is no `'name'` tag.\n\n*Method `TagSet.save_as_ini(self, f, section: str, config=None)`*:\nSave this `TagSet` to the config file `f` as `section`.\n\nIf `f` is a string, read an existing config from that file\nand update the section.\n\n*Method `TagSet.set(self, tag_name, value, *, verbose=None)`*:\nSet `self[tag_name]=value`.\nIf `verbose`, emit an info message if this changes the previous value.\n\n*Method `TagSet.set_from(self, other, verbose=None)`*:\nCompletely replace the values in `self`\nwith the values from `other`,\na `TagSet` or any other `name`=>`value` dict.\n\nThis has the feature of logging changes\nby calling `.set` and `.discard` to effect the changes.\n\n*Method `TagSet.subtags(self, prefix, as_tagset=False)`*:\nReturn `TagSetPrefixView` of the tags commencing with `prefix+'.'`\nwith the key prefixes stripped off.\n\nIf `as_tagset` is true (default `False`)\nreturn a new standalone `TagSet` containing the prefixed keys.\n\nExample:\n\n    >>> tags = TagSet({'a.b':1, 'a.d':2, 'c.e':3})\n    >>> tags.subtags('a')\n    TagSetPrefixView:a.{'b': 1, 'd': 2}\n    >>> tags.subtags('a', as_tagset=True)\n    TagSet:{'b': 1, 'd': 2}\n\n*Method `TagSet.tag(self, tag_name, prefix=None, ontology=None)`*:\nReturn a `Tag` for `tag_name`, or `None` if missing.\n\nParameters:\n* `tag_name`: the name of the `Tag` to create\n* `prefix`: optional prefix;\n  if supplied, prepend `prefix+'.'` to the `Tag` name\n* `ontology`: optional ontology for the `Tag`,\n  default `self.ontology`\n\n*Method `TagSet.tag_metadata(self, tag_name, prefix=None, ontology=None, convert=None)`*:\nReturn a list of the metadata for the `Tag` named `tag_name`,\nor an empty list if the `Tag` is missing.\n\n*Property `TagSet.unixtime`*:\n`unixtime` property, autosets to `time.time()` if accessed and missing.\n\n*Method `TagSet.update(self, other=None, *, prefix=None, verbose=None, **kw)`*:\nUpdate this `TagSet` from `other`,\na dict of `{name:value}`\nor an iterable of `Tag`like or `(name,value)` things.\n\n*Property `TagSet.uuid`*:\nThe `TagSet`'s `'uuid'` value as a UUID if present, otherwise `None`.\n\n## Class `TagSetCriterion(cs.deco.Promotable)`\n\nA testable criterion for a `TagSet`.\n\n*`TagSetCriterion.TAG_BASED_TEST_CLASS`*\n\n*Method `TagSetCriterion.from_any(o)`*:\nConvert some suitable object `o` into a `TagSetCriterion`.\n\nVarious possibilities for `o` are:\n* `TagSetCriterion`: returned unchanged\n* `str`: a string tests for the presence\n  of a tag with that name and optional value;\n* an object with a `.choice` attribute;\n  this is taken to be a `TagSetCriterion` ducktype and returned unchanged\n* an object with `.name` and `.value` attributes;\n  this is taken to be `Tag`-like and a positive test is constructed\n* `Tag`: an object with a `.name` and `.value`\n  is equivalent to a positive equality `TagBasedTest`\n* `(name,value)`: a 2 element sequence\n  is equivalent to a positive equality `TagBasedTest`\n\n*Method `TagSetCriterion.from_arg(arg, fallback_parse=None)`*:\nPrepare a `TagSetCriterion` from the string `arg`\nwhere `arg` is known to be entirely composed of the value,\nsuch as a command line argument.\n\nThis calls the `from_str` method with `fallback_parse` set\nto gather then entire tail of the supplied string `arg`.\n\n*Method `TagSetCriterion.from_str(s: str, fallback_parse=None)`*:\nPrepare a `TagSetCriterion` from the string `s`.\n\n*Method `TagSetCriterion.from_str2(s, offset=0, delim=None, fallback_parse=None)`*:\nParse a criterion from `s` at `offset` and return `(TagSetCriterion,offset)`.\n\nThis method recognises an optional leading `'!'` or `'-'`\nindicating negation of the test,\nfollowed by a criterion recognised by the `.parse` method\nof one of the classes in `cls.CRITERION_PARSE_CLASSES`.\n\n*Method `TagSetCriterion.match_tagged_entity(self, te: 'TagSet') -> bool`*:\nApply this `TagSetCriterion` to a `TagSet`.\n\n## Class `TagSetPrefixView(cs.lex.FormatableMixin)`\n\nA view of a `TagSet` via a `prefix`.\n\nAccess to a key `k` accesses the `TagSet`\nwith the key `prefix+'.'+k`.\n\nThis is a kind of funny hybrid of a `Tag` and a `TagSet`\nin that some things such as `__format__`\nwill format the `Tag` named `prefix` if it exists\nin preference to the subtags.\n\nExample:\n\n    >>> tags = TagSet(a=1, b=2)\n    >>> tags\n    TagSet:{'a': 1, 'b': 2}\n    >>> tags['sub.x'] = 3\n    >>> tags['sub.y'] = 4\n    >>> tags\n    TagSet:{'a': 1, 'b': 2, 'sub.x': 3, 'sub.y': 4}\n    >>> sub = tags.sub\n    >>> sub\n    TagSetPrefixView:sub.{'x': 3, 'y': 4}\n    >>> sub.z = 5\n    >>> sub\n    TagSetPrefixView:sub.{'x': 3, 'y': 4, 'z': 5}\n    >>> tags\n    TagSet:{'a': 1, 'b': 2, 'sub.x': 3, 'sub.y': 4, 'sub.z': 5}\n\n*Method `TagSetPrefixView.__getattr__(self, attr)`*:\nProxy other attributes through to the `TagSet`.\n\n*Method `TagSetPrefixView.__setattr__(self, attr, value)`*:\nAttribute based `Tag` access.\n\nIf `attr` is in `self.__dict__` then that is updated,\nsupporting \"normal\" attributes set on the instance.\nOtherwise the `Tag` named `attr` is set to `value`.\n\nThe `__init__` methods of subclasses should do something like this\n(from `TagSet.__init__`)\nto set up the ordinary instance attributes\nwhich are not to be treated as `Tag`s:\n\n    self.__dict__.update(id=_id, ontology=_ontology, modified=False)\n\n*Method `TagSetPrefixView.as_dict(self)`*:\nReturn a `dict` representation of this view.\n\n*Method `TagSetPrefixView.get(self, k, default=None)`*:\nMapping `get` method.\n\n*Method `TagSetPrefixView.get_format_attribute(self, attr)`*:\nFetch a formatting attribute from the proxied object.\n\n*Method `TagSetPrefixView.items(self)`*:\nReturn an iterable of the items (`Tag` name, `Tag`).\n\n*Method `TagSetPrefixView.keys(self)`*:\nThe keys of the subtags.\n\n*Property `TagSetPrefixView.ontology`*:\nThe ontology of the references `TagSet`.\n\n*Method `TagSetPrefixView.setdefault(self, k, v=None)`*:\nMapping `setdefault` method.\n\n*Method `TagSetPrefixView.subtags(self, subprefix)`*:\nReturn a deeper view of the `TagSet`.\n\n*Property `TagSetPrefixView.tag`*:\nThe `Tag` for the prefix, or `None` if there is no such `Tag`.\n\n*Method `TagSetPrefixView.update(self, mapping)`*:\nUpdate tags from a name->value mapping.\n\n*Property `TagSetPrefixView.value`*:\nReturn the `Tag` value for the prefix, or `None` if there is no such `Tag`.\n\n*Method `TagSetPrefixView.values(self)`*:\nReturn an iterable of the values (`Tag`s).\n\n## Class `TagSetsSubdomain(cs.obj.SingletonMixin, cs.mappings.PrefixedMappingProxy)`\n\nA view into a `BaseTagSets` for keys commencing with a prefix\nbeing the subdomain plus a dot (`'.'`).\n\n*Property `TagSetsSubdomain.TAGGED_ENTITY_FACTORY`*:\nThe entity factory comes from the parent collection.\n\n## Class `TagsOntology(cs.obj.SingletonMixin, BaseTagSets)`\n\nAn ontology for tag names.\nThis is based around a mapping of names\nto ontological information expressed as a `TagSet`.\n\nNormally an object's tags are not a self contained repository of all the information;\ninstead a tag just names some information.\n\nAs a example, consider the tag `colour=blue`.\nMeta information about `blue` is obtained via the ontology,\nwhich has an entry for the colour `blue`.\nWe adopt the convention that the type is just the tag name,\nso we obtain the metadata by calling `ontology.metadata(tag)`\nor alternatively `ontology.metadata(tag.name,tag.value)`\nbeing the type name and value respectively.\n\nThe ontology itself is based around `TagSets` and effectively the call\n`ontology.metadata('colour','blue')`\nwould look up the `TagSet` named `colour.blue` in the underlying `Tagsets`.\n\nFor a self contained dataset this means that it can be its own ontology.\nFor tags associated with arbitrary objects\nsuch as the filesystem tags maintained by `cs.fstags`\nthe ontology would be a separate tags collection stored in a central place.\n\nThere are two main categories of entries in an ontology:\n* metadata: other entries named *typename*`.`*value_key*\n  contains a `TagSet` holding metadata for a value of type *typename*\n  whose value is mapped to *value_key*\n* types: an optional entry named `type.`*typename* contains a `TagSet`\n  describing the type named *typename*;\n  really this is just more metadata where the \"type name\" is `type`\n\nMetadata are `TagSets` instances describing particular values of a type.\nFor example, some metadata for the `Tag` `colour=\"blue\"`:\n\n    colour.blue url=\"https://en.wikipedia.org/wiki/Blue\" wavelengths=\"450nm-495nm\"\n\nSome metadata associated with the `Tag` `actor=\"Scarlett Johansson\"`:\n\n    actor.scarlett_johansson role=[\"Black Widow (Marvel)\"]\n    character.marvel.black_widow fullname=[\"Natasha Romanov\"]\n\nThe tag values are lists above because an actor might play many roles, etc.\n\nThere's a convention for converting human descriptions\nsuch as the role string `\"Black Widow (Marvel)\"` to its metadata.\n* the value `\"Black Widow (Marvel)\"` if converted to a key\n  by the ontology method `value_to_tag_name`;\n  it moves a bracket suffix such as `(Marvel)` to the front as a prefix\n  `marvel.` and downcases the rest of the string and turns spaces into underscores.\n  This yields the value key `marvel.black_widow`.\n* the type is `role`, so the ontology entry for the metadata\n  is `role.marvel.black_widow`\n\nThis requires type information about a `role`.\nHere are some type definitions supporting the above metadata:\n\n    type.person type=str description=\"A person.\"\n    type.actor type=person description=\"An actor's stage name.\"\n    type.character type=str description=\"A person in a story.\"\n    type.role type_name=character description=\"A character role in a performance.\"\n    type.cast type=dict key_type=actor member_type=role description=\"Cast members and their roles.\"\n\nThe basic types have their Python names: `int`, `float`, `str`, `list`,\n`dict`, `date`, `datetime`.\nYou can define subtypes of these for your own purposes\nas illustrated above.\n\nFor example:\n\n    type.colour type=str description=\"A hue.\"\n\nwhich subclasses `str`.\n\nSubtypes of `list` include a `member_type`\nspecifying the type for members of a `Tag` value:\n\n    type.scene type=list member_type=str description=\"A movie scene.\"\n\nSubtypes of `dict` include a `key_type` and a `member_type`\nspecifying the type for keys and members of a `Tag` value:\n\nAccessing type data and metadata:\n\nA `TagSet` may have a reference to a `TagsOntology` as `.ontology`\nand so also do any of its `Tag`s.\n\n*Method `TagsOntology.__bool__(self)`*:\nSupport easy `ontology or some_default` tests,\nsince ontologies are broadly optional.\n\n*Method `TagsOntology.__delitem__(self, name)`*:\nDelete the entity named `name`.\n\n*Method `TagsOntology.__getitem__(self, name)`*:\nFetch `tags` for the entity named `name`.\n\n*Method `TagsOntology.__setitem__(self, name, tags)`*:\nApply `tags` to the entity named `name`.\n\n*Method `TagsOntology.add_tagsets(self, tagsets: cs.tagset.BaseTagSets, match, unmatch=None, index=0)`*:\nInsert a `_TagsOntology_SubTagSets` at `index`\nin the list of `_TagsOntology_SubTagSets`es.\n\nThe new `_TagsOntology_SubTagSets` instance is initialised\nfrom the supplied `tagsets`, `match`, `unmatch` parameters.\n\n*Method `TagsOntology.as_dict(self)`*:\nReturn a `dict` containing a mapping of entry names to their `TagSet`s.\n\n*Method `TagsOntology.basetype(self, typename)`*:\nInfer the base type name from a type name.\nThe default type is `'str'`,\nbut any type which resolves to one in `self.BASE_TYPES`\nmay be returned.\n\n*Method `TagsOntology.by_type(self, type_name, with_tagsets=False)`*:\nYield keys or (key,tagset) of type `type_name`\ni.e. all keys commencing with *type_name*`.`.\n\n*Method `TagsOntology.convert_tag(self, tag)`*:\nConvert a `Tag`'s value accord to the ontology.\nReturn a new `Tag` with the converted value\nor the original `Tag` unchanged.\n\nThis is primarily aimed at things like regexp based autotagging,\nwhere the matches are all strings\nbut various fields have special types,\ncommonly `int`s or `date`s.\n\n*Method `TagsOntology.edit_indices(self, indices, prefix=None)`*:\nEdit the entries specified by indices.\nReturn `TagSet`s for the entries which were changed.\n\n*Method `TagsOntology.from_match(tagsets, match, unmatch=None)`*:\nInitialise a `SubTagSets` from `tagsets`, `match` and optional `unmatch`.\n\nParameters:\n* `tagsets`: a `TagSets` holding ontology information\n* `match`: a match function used to choose entries based on a type name\n* `unmatch`: an optional reverse for `match`, accepting a subtype\n  name and returning its public name\n\nIf `match` is `None`\nthen `tagsets` will always be chosen if no prior entry matched.\n\nOtherwise, `match` is resolved to a function `match-func(type_name)`\nwhich returns a subtype name on a match and a false value on no match.\n\nIf `match` is a callable it is used as `match_func` directly.\n\nif `match` is a list, tuple or set\nthen this method calls itself with `(tagsets,submatch)`\nfor each member `submatch` if `match`.\n\nIf `match` is a `str`,\nif it ends in a dot '.', dash '-' or underscore '_'\nthen it is considered a prefix of `type_name` and the returned\nsubtype name is the text from `type_name` after the prefix\nothwerwise it is considered a full match for the `type_name`\nand the returns subtype name is `type_name` unchanged.\nThe `match` string is a simplistic shell style glob\nsupporting `*` but not `?` or `[`*seq*`]`.\n\nThe value of `unmatch` is constrained by `match`.\nIf `match` is `None`, `unmatch` must also be `None`;\nthe type name is used unchanged.\nIf `match` is callable`, `unmatch` must also be callable;\nit is expected to reverse `match`.\n\nExamples:\n\n    >>> from cs.sqltags import SQLTags\n    >>> from os.path import expanduser as u\n    >>> # an initial empty ontology with a default in memory mapping\n    >>> ont = TagsOntology()\n    >>> # divert the types actor, role and series to my media ontology\n    >>> ont.add_tagsets(\n    ...     SQLTags(u('~/var/media-ontology.sqlite')),\n    ...     ['actor', 'role', 'series'])\n    >>> # divert type \"musicbrainz.recording\" to mbdb.sqlite\n    >>> # mapping to the type \"recording\"\n    >>> ont.add_tagsets(SQLTags(u('~/.cache/mbdb.sqlite')), 'musicbrainz.')\n    >>> # divert type \"tvdb.actor\" to tvdb.sqlite\n    >>> # mapping to the type \"actor\"\n    >>> ont.add_tagsets(SQLTags(u('~/.cache/tvdb.sqlite')), 'tvdb.')\n\n*Method `TagsOntology.get(self, name, default=None)`*:\nFetch the entity named `name` or `default`.\n\n*Method `TagsOntology.items(self)`*:\nYield `(entity_name,tags)` for all the items in each subtagsets.\n\n*Method `TagsOntology.keys(self)`*:\nYield entity names for all the entities.\n\n*Method `TagsOntology.metadata(self, type_name, value, *, convert=None)`*:\nReturn the metadata `TagSet` for `type_name` and `value`.\nThis implements the mapping between a type's value and its semantics.\n\nThe optional parameter `convert`\nmay specify a function to use to convert `value` to a tag name component\nto be used in place of `self.value_to_tag_name` (the default).\n\nFor example, if a `TagSet` had a list of characters such as:\n\n    character=[\"Captain America (Marvel)\",\"Black Widow (Marvel)\"]\n\nthen these values could be converted to the dotted identifiers\n`character.marvel.captain_america`\nand `character.marvel.black_widow` respectively,\nready for lookup in the ontology\nto obtain the \"metadata\" `TagSet` for each specific value.\n\n*Method `TagsOntology.startup_shutdown(self)`*:\nOpen all the sub`TagSets` and close on exit.\n\n*Method `TagsOntology.subtype_name(self, type_name)`*:\nReturn the type name for use within `self.tagsets` from `type_name`.\nReturns `None` if this is not a supported `type_name`.\n\n*Method `TagsOntology.type_name(self, subtype_name)`*:\nReturn the external type name from the internal `subtype_name`\nwhich is used within `self.tagsets`.\n\n*Method `TagsOntology.type_names(self)`*:\nReturn defined type names i.e. all entries starting `type.`.\n\n*Method `TagsOntology.type_values(self, type_name, value_tag_name=None)`*:\nYield the various defined values for `type_name`.\nThis is useful for types with enumerated metadata entries.\n\nFor example, if metadata entries exist as `foo.bah` and `foo.baz`\nfor the `type_name` `'foo'`\nthen this yields `'bah'` and `'baz'`.`\n\nNote that this looks for a `Tag` for the value,\nfalling back to the entry suffix if the tag is not present.\nThat tag is normally named `value`\n(from DEFAULT_VALUE_TAG_NAME)\nbut may be overridden by the `value_tag_name` parameter.\nAlso note that normally it is desireable that the value\nconvert to the suffix via the `value_to_tag_name` method\nso that the metadata entry can be located from the value.\n\n*Method `TagsOntology.typedef(self, type_name)`*:\nReturn the `TagSet` defining the type named `type_name`.\n\n*Method `TagsOntology.types(self)`*:\nGenerator yielding defined type names and their defining `TagSet`.\n\n*Method `TagsOntology.value_to_tag_name(value)`*:\nConvert a tag value to a tagnamelike dotted identifierish string\nfor use in ontology lookup.\nRaises `ValueError` for unconvertable values.\n\nWe are allowing dashes in the result (UUIDs, MusicBrainz discids, etc).\n\n`int`s are converted to `str`.\n\nStrings are converted as follows:\n* a trailing `(.*)` is turned into a prefix with a dot,\n  for example `\"Captain America (Marvel)\"`\n  becomes `\"Marvel.Captain America\"`.\n* the string is split into words (nonwhitespace),\n  lowercased and joined with underscores,\n  for example `\"Marvel.Captain America\"`\n  becomes `\"marvel.captain_america\"`.\n\n## Class `TagsOntologyCommand(cs.cmdutils.BaseCommand)`\n\nA command line for working with ontology types.\n\nCommand line implementation.\n\nUsage summary:\n\n    Usage: tagsontology subcommand [...]\n      Subcommands:\n        edit [{/name-regexp | entity-name}]\n          Edit entities.\n          With no arguments, edit all the entities.\n          With an argument starting with a slash, edit the entities\n          whose names match the regexp.\n          Otherwise the argument is expected to be an entity name;\n          edit the tags of that entity.\n        help [-l] [subcommand-names...]\n          Print help for subcommands.\n          This outputs the full help for the named subcommands,\n          or the short help for all subcommands if no names are specified.\n          -l  Long help even if no subcommand-names provided.\n        meta tag=value\n        shell\n          Run a command prompt via cmd.Cmd using this command's subcommands.\n        type\n            With no arguments, list the defined types.\n          type type_name\n            With a type name, print its `Tag`s.\n          type type_name edit\n            Edit the tags defining a type.\n          type type_name edit meta_names_pattern...\n            Edit the tags for the metadata names matching the\n            meta_names_patterns.\n          type type_name list\n          type type_name ls\n            List the metadata names for this type and their tags.\n          type type_name + entity_name [tags...]\n            Create type_name.entity_name and apply the tags.\n\n*Method `TagsOntologyCommand.cmd_edit(self, argv)`*:\nUsage: {cmd} [{{/name-regexp | entity-name}}]\nEdit entities.\nWith no arguments, edit all the entities.\nWith an argument starting with a slash, edit the entities\nwhose names match the regexp.\nOtherwise the argument is expected to be an entity name;\nedit the tags of that entity.\n\n*Method `TagsOntologyCommand.cmd_meta(self, argv)`*:\nUsage: {cmd} tag=value\n\n*Method `TagsOntologyCommand.cmd_type(self, argv)`*:\nUsage:\n{cmd}\n  With no arguments, list the defined types.\n{cmd} type_name\n  With a type name, print its `Tag`s.\n{cmd} type_name edit\n  Edit the tags defining a type.\n{cmd} type_name edit meta_names_pattern...\n  Edit the tags for the metadata names matching the\n  meta_names_patterns.\n{cmd} type_name list\n{cmd} type_name ls\n  List the metadata names for this type and their tags.\n{cmd} type_name + entity_name [tags...]\n  Create type_name.entity_name and apply the tags.\n\n# Release Log\n\n\n\n*Release 20240422.2*:\njsonable: use obj.for_json() if available.\n\n*Release 20240422.1*:\njsonable: convert pathlib.PurePath to str, hoping this isn't too open ended a can of worms.\n\n*Release 20240422*:\n* New jsonable(obj) function to return a deep copy of `obj` which can be transcribed as JSON.\n* Tag.transcribe_value: pass jsonable(value) to the JSON encoder, drop special checks now done by jsonable().\n* Tag.__str__: do not catch TypeError any more, was embedding Python repr()s in .fstags files - now Tag.transcribe_value() does the correct thing where that is possible.\n\n*Release 20240316*:\nFixed release upload artifacts.\n\n*Release 20240305*:\n* Tag.from_str2: make the ontology optional.\n* TagSetPrefixView: provide __len__() and update().\n\n*Release 20240211*:\n* TagFile.parse_tag_line: recognise dotted_identifiers directly, avoids misparsing bare \"nan\" as float NaN.\n* Tag.parse_value: BUGFIX parse - always to the primary types first (int, float) before trying any funny extra types.\n\n*Release 20240201*:\nTagsOntology.metadata: actually call the .items() method!\n\n*Release 20231129*:\n* TagSet.__getattr__: rework the attribute lookup with greater precision.\n* TagSetPrefixView.__getattr__: if the attribute is not there, raise Attribute error, do not try to fall back to something else.\n* TagSet: drop ATTRABLE_MAPPING_DEFAULT=None, caused far more confusion that it was worth.\n\n*Release 20230612*:\n* TagFile.save_tagsets: catch and warn about exceptions from update_mapping[key].update, something is wrong with my SQLTags usage.\n* TagFile.save_tagsets: update_mapping: do not swallow AttributeError.\n\n*Release 20230407*:\nMove the (optional) ORM open/close from FSTags.startup_shutdown to TagFile.save, greatly shortens the ORM lock.\n\n*Release 20230212*:\nMark TagSetCriterion as Promotable.\n\n*Release 20230210*:\n* TagFile: new optional update_mapping secondary mapping to which to mirror file tags, for example to an SQLTags.\n* New .uuid:UUID property returning the UUID for the tag named 'uuid' or None.\n\n*Release 20230126*:\nNew TagSet.is_stale() method based on .expiry attribute, intended for TagSets which are caches of other primary data.\n\n*Release 20221228*:\n* TagFile: drop _singleton_key, FSPathBasedSingleton provides a good default.\n* TagFile.save_tagsets,tags_line: new optional prune=False parameter to drop empty top level dict/lists.\n* TagFile.save: plumb prune=False parameter.\n\n*Release 20220806*:\nNew TagSetCriterion.promote(obj)->TagSetCriterion class method.\n\n*Release 20220606*:\n* Tag.parse_value: bugfix parse of float.\n* TagSet.edit: accept optional comments parameter with addition header comment lines, be more tolerant of errors, avoid losing data on error.\n\n*Release 20220430*:\n* TagSetPrefixView: new as_dict() method.\n* TagSetPrefixView.__str__: behave like TagSet.__str__.\n* TagFile.save_tagsets: do not try to save if the file is missing and the tagsets are empty.\n* New TagSet.from_tags(tags) factory to make a new TagSet from an iterable of tags.\n* TagSetPrefixView: add .get and .setdefault mapping methods.\n* RegexpTagRule: accept optional tag_prefix parameter.\n* Tagset: new from_ini() and save_as_ini() methods to support cs.timeseries config files, probably handy elsewhere.\n\n*Release 20220311*:\nAssorted internal changes.\n\n*Release 20211212*:\n* Tag: new fallback_parse parameter for value parsing, default get_nonwhite.\n* Tag: new from_arg factory with fallback_parse grabbing the whole string for command line arguments, thus supporting unquoted strings for ease of use.\n* TagSetCriterion: new optional fallback_parse parameter and from_arg method as for the Tag factories.\n* Tag.transcribe_value: accept optional json_options to control the JSON encoder, used for human friendly multiline edits in cs.app.tagger.\n* Rename edit_many to edit_tagsets for clarity.\n* TagsOntology: new type_values method to return values for a type (derived from their metadata entries).\n* Tag: new alt_values method returning its TagsOntology.type_values.\n* (Internal) New _FormatStringTagProxy which proxies a Tag but uses str(self.__proxied.value) for __str__ to support format strings.\n* (Internal) TagSet.get_value: if arg_name matches a Tag, return a _FormatStringTagProxy.\n* Tag.__new__: accept (tag_name,value) or (Tag) as initialisation parameters.\n\n*Release 20210913*:\n* TagSet.get_value: raise KeyError in strict mode, leave placeholder otherwise.\n* Other small changes.\n\n*Release 20210906*:\nMany many updates; some semantics have changed.\n\n*Release 20210428*:\nBugfix TagSet.set: internal in place changes to a complex tag value were not noticed, causing TagFile to not update on shutdown.\n\n*Release 20210420*:\n* TagSet: also subclass cs.dateutils.UNIXTimeMixin.\n* Various TagSetNamespace updates and bugfixes.\n\n*Release 20210404*:\nBugfix TagBasedTest.COMPARISON_FUNCS[\"=\"]: if cmp_value is None, return true (the tag is present).\n\n*Release 20210306*:\n* ExtendedNamespace,TagSetNamespace: move the .[:alpha:]* attribute support from ExtendedNamespace to TagSetNamespace because it requires Tags.\n* TagSetNamespace.__getattr__: new _i, _s, _f suffixes to return int, str or float tag values (or None); fold _lc in with these.\n* Pull most of `TaggedEntity` out into `TaggedEntityMixin` for reuse by domain specific tagged entities.\n* TaggedEntity: new .set and .discard methods.\n* TaggedEntity: new as_editable_line, from_editable_line, edit and edit_entities methods to support editing entities using a text editor.\n* ontologies: type entries are now prefixed with \"type.\" and metadata entries are prefixed with \"meta.\"; provide a worked ontology example in the introduction and improve related docstrings.\n* TagsOntology: new .types(), .types_names(), .meta(type_name,value), .meta_names() methods.\n* TagsOntology.__getitem__: create missing TagSets on demand.\n* New TagsOntologyCommand, initially with a \"type [type_name [{edit|list}]]\" subcommand, ready for use as the cmd_ont subcommand of other tag related commands.\n* TagSet: support initialisation like a dict including keywords, and move the `ontology` parameter to `_onotology`.\n* TagSet: include AttrableMappingMixin to enable attribute access to values when there is no conflict with normal methods.\n* UUID encode/decode support.\n* Honour $TAGSET_EDITOR or $EDITOR as preferred interactive editor for tags.\n* New TagSet.subtags(prefix) to extract a subset of the tags.\n* TagsOntology.value_metadata: new optional convert parameter to override the default \"convert human friendly name\" algorithm, particularly to pass convert=str to things which are already the basic id.\n* Rename TaggedEntity to TagSet.\n* Rename TaggedEntities to TagSets.\n* TagSet: new csvrow and from_csvrow methods imported from obsolete TaggedEntityMixin class.\n* Move BaseTagFile from cs.fstags to TagFile in cs.tagset.\n* TagSet: support access to the tag \"c.x\" via attributes provided there is no \"c\" tag in the way.\n* TagSet.unixtime: implement the autoset-to-now semantics.\n* New as_timestamp(): convert date, datetime, int or float to a UNIX timestamp.\n* Assorted docstring updates and bugfixes.\n\n*Release 20200716*:\n* Update for changed cs.obj.SingletonMixin API.\n* Pull in TaggedEntity from cs.sqltags and add the .csvrow property and the .from_csvrow factory.\n\n*Release 20200521.1*:\nFix DISTINFO.install_requires, drop debug import.\n\n*Release 20200521*:\n* New ValueDetail and KeyValueDetail classes for returning ontology information; TagInfo.detail now returns a ValueDetail for scalar types, a list of ValueDetails for sequence types and a list of KeyValueDetails for mapping types; drop various TagInfo mapping/iterable style methods, too confusing to use.\n* Plumb ontology parameter throughout, always optional.\n* Drop TypedTag, Tags now use ontologies for this.\n* New TagsCommandMixin to support BaseCommands which manipulate Tags.\n* Many improvements and bugfixes.\n\n*Release 20200318*:\n* *Note that the TagsOntology stuff is in flux and totally alpha.*\n* Tag.prefix_name factory returning a new tag if prefix is not empty, ptherwise self.\n* TagSet.update: accept an optional prefix for inserting \"foreign\" tags with a distinguishing name prefix.\n* Tag.as_json: turn sets and tuples into lists for encoding.\n* Backport for Python < 3.7 (no fromisoformat functions).\n* TagSet: drop unused and illplaced .titleify, .episode_title and .title methods.\n* TagSet: remove \"defaults\", unused.\n* Make TagSet a direct subclass of dict, adjust uses of .update etc.\n* New ExtendedNamespace class which is a SimpleNamespace with some inferred attributes and a partial mapping API (keys and __getitem__).\n* New TagSet.ns() returning the Tags as an ExtendedNamespace, which doubles as a mapping for str.format_map; TagSet.format_kwargs is now an alias for this.\n* New Tag.from_string factory to parse a str into a Tag.\n* New TagsOntology and TypedTag classes to provide type and value-detail information; very very alpha and subject to change.\n\n*Release 20200229.1*:\nInitial release: pull TagSet, Tag, TagChoice from cs.fstags for independent use.\n\n",
    "bugtrack_url": null,
    "license": "GNU General Public License v3 or later (GPLv3+)",
    "summary": "Tags and sets of tags with __format__ support and optional ontology information.",
    "version": "20240422.2",
    "project_urls": {
        "URL": "https://bitbucket.org/cameron_simpson/css/commits/all"
    },
    "split_keywords": [
        "python3"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "920ff881013cb1a513e6239778181d362272d5057c87cd34544b2fad64002d62",
                "md5": "8e93dcdf7997264b8e7001bce8d0a665",
                "sha256": "acc50d9ad2e926c5ad1b83020132344a7f4f6a8d9cb58c1390fe3fd418786015"
            },
            "downloads": -1,
            "filename": "cs.tagset-20240422.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "8e93dcdf7997264b8e7001bce8d0a665",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 57580,
            "upload_time": "2024-04-22T06:32:51",
            "upload_time_iso_8601": "2024-04-22T06:32:51.016237Z",
            "url": "https://files.pythonhosted.org/packages/92/0f/f881013cb1a513e6239778181d362272d5057c87cd34544b2fad64002d62/cs.tagset-20240422.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c202f914b20001439c86522dca27f36ec66195aa4d729f92d5399afc10787479",
                "md5": "94e64aa4cf8d3bd578e1eb4b70cb3a19",
                "sha256": "422fa18ebed6693923d2921842edb0dbc1bc3c5d6d467990a1ac574a64dc7f68"
            },
            "downloads": -1,
            "filename": "cs.tagset-20240422.2.tar.gz",
            "has_sig": false,
            "md5_digest": "94e64aa4cf8d3bd578e1eb4b70cb3a19",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 116356,
            "upload_time": "2024-04-22T06:32:54",
            "upload_time_iso_8601": "2024-04-22T06:32:54.261348Z",
            "url": "https://files.pythonhosted.org/packages/c2/02/f914b20001439c86522dca27f36ec66195aa4d729f92d5399afc10787479/cs.tagset-20240422.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-22 06:32:54",
    "github": false,
    "gitlab": false,
    "bitbucket": true,
    "codeberg": false,
    "bitbucket_user": "cameron_simpson",
    "bitbucket_project": "css",
    "lcname": "cs.tagset"
}
        
Elapsed time: 0.23484s