cs-sqltags 20260531__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,903 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs-sqltags
3
+ Version: 20260531
4
+ Summary: Simple SQL based tagging and the associated `sqltags` command line script, supporting both tagged named objects and tagged timestamped log entries.
5
+ Keywords: python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Requires-Dist: cs.cmdutils>=20210404
16
+ Requires-Dist: cs.context>=20250528
17
+ Requires-Dist: cs.dateutils>=20250724
18
+ Requires-Dist: cs.deco>=20260525
19
+ Requires-Dist: cs.fileutils>=20260531
20
+ Requires-Dist: cs.lex>=20260526
21
+ Requires-Dist: cs.logutils>=20250323
22
+ Requires-Dist: cs.obj>=20260526
23
+ Requires-Dist: cs.pfx>=20250914
24
+ Requires-Dist: cs.sqlalchemy_utils>=20210420
25
+ Requires-Dist: cs.tagset>=20211212
26
+ Requires-Dist: cs.threads>=20201025
27
+ Requires-Dist: cs.upd>=20260526
28
+ Requires-Dist: icontract
29
+ Requires-Dist: sqlalchemy
30
+ Requires-Dist: typeguard
31
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
32
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
33
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
34
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/sqltags.py
35
+
36
+ Simple SQL based tagging
37
+ and the associated `sqltags` command line script,
38
+ supporting both tagged named objects and tagged timestamped log entries.
39
+
40
+ *Latest release 20260531*:
41
+ * BaseSQLTagsCommand: modernise usage spec and common options, make .sqltags a cached property of BaseSQLTagsCommand (uses self.TAGSETS_CLASS, hence not in the Options).
42
+ * SQLTagSet: provide __lt__ to order entities.
43
+ * SQLTags now subclasses SingletonMixin, keying on the normalised db URL for filesystem paths.
44
+ * SQLTagSet.jsonable: turn datetimes into ISO8601 strings.
45
+ * SQLTags.__getitem__: initial support for a (tag_name,value) 2-tuple as an index.
46
+ * SQLTags.__getitem__: if the (missing) index is a (name,value) 2-tuple create the entity with name=None and set the tag.
47
+ * SQLTags.__getitem__: for a (tag_name,tag_value) 2-tuple, if the tag_name is "name" create it with that name.
48
+ * SQLTagSet.TYPE_JS_MAPPING: add UUID to the supported types.
49
+ * SQLTagBasedTest.sql_parameters: support name=None.
50
+ * SQLTagSet: new .deref() method to dereference tags whose values refer to other SQLTagSets.
51
+ * SQLTagsCommandsMixin: rename cmd_init to cmd_dbinit.
52
+ * New HasSQLTags(HasTags) mixin providing a .tags_db proptery from self.tags.sqltags.
53
+ * New UsesSQLTags mixin providing .__getitem__(type_subname,key).
54
+ * cs.sqltags,cs.cdrip: move the MBDB.find method into UsesSQLTags.
55
+ * Drop HasSQLTags (HasTags does it all now), make UsesSQLTags subclass UsesTagSets and just set TagSetsClass=SQLTags.
56
+ * New SQLTags.dbshell() method to run an interactive db prompt.
57
+ * SQLTags: new .preload(*criteria) method to preload and return TagSets.
58
+ * tagset,fstags,sqltags: make _id only special to SQLTagSet (the db row id), drop all mentions elsewhere.
59
+ * SQLTagSet.set: new optional force=False parameter, skips db update if false and value unchanged.
60
+ * SQLTags.default_factory: drop skip_refresh support, will be covered by the Refreshable stuff.
61
+
62
+ Compared to `cs.fstags` and its associated `fstags` command,
63
+ this is oriented towards large numbers of items
64
+ not naturally associated with filesystem objects.
65
+
66
+ My initial use case is an activity log
67
+ (unnamed timestamped tag sets)
68
+ but I'm also using it for ontologies
69
+ (named tag sets containing metadata).
70
+
71
+ Many basic tasks can be performed with the `sqltags` command line utility,
72
+ documented under the `SQLTagsCommand` class below.
73
+
74
+ See the `SQLTagsORM` documentation for details about how data
75
+ are stored in the database.
76
+ See the `SQLTagSet` documentation for details of how various
77
+ tag value types are supported.
78
+
79
+ Short summary:
80
+ * `BaseSQLTagsCommand`: Common features for commands oriented around an `SQLTags` database.
81
+ * `glob2like`: Convert a filename glob to an SQL LIKE pattern.
82
+ * `main`: Command line mode.
83
+ * `PolyValue`: A `namedtuple` for the polyvalues used in an `SQLTagsORM`.
84
+ * `PolyValueColumnMixin`: A mixin for classes with `(float_value,string_value,structured_value)` columns. This is used by the `Tags` and `TagMultiValues` relations inside `SQLTagsORM`.
85
+ * `PolyValued`: A mixin for classes with `(float_value,string_value,structured_value)` columns.
86
+ * `prefix2like`: Convert a prefix string to an SQL LIKE pattern.
87
+ * `SQLParameters`: The parameters required for constructing queries or extending queries with JOINs.
88
+ * `SQLTagBasedTest`: A `cs.tagset.TagBasedTest` extended with a `.sql_parameters` method.
89
+ * `SQLTagProxies`: A proxy for the tags supporting Python comparison => `SQLParameters`.
90
+ * `SQLTagProxy`: An object based on a `Tag` name which produces an `SQLParameters` when compared with some value.
91
+ * `SQLTags`: A class using an SQL database to store its `TagSet`s.
92
+ * `SQLTagsCommand`: `sqltags` main command line utility.
93
+ * `SQLTagSet`: A singleton `TagSet` attached to an `SQLTags` instance.
94
+ * `SQLTagsORM`: The ORM for an `SQLTags`.
95
+ * `SQTCriterion`: Subclass of `TagSetCriterion` requiring an `.sql_parameters` method which returns an `SQLParameters` providing the information required to construct an sqlalchemy query. It also resets `.CRITERION_PARSE_CLASSES`, which will pick up the SQL capable criterion classes below.
96
+ * `SQTEntityIdTest`: A test on `entity.id`.
97
+ * `UsesSQLTags`: A mixin subclassing `UsesTagSets` to support classes which use an `SQLTags` to store their data.
98
+ * `verbose`: Emit message if in verbose mode.
99
+
100
+ Module contents:
101
+ - <a name="BaseSQLTagsCommand"></a>`class BaseSQLTagsCommand(cs.cmdutils.BaseCommand, SQLTagsCommandsMixin)`: Common features for commands oriented around an `SQLTags` database.
102
+
103
+ Usage summary:
104
+
105
+ Usage: basesqltags [common-options...] subcommand [options...]
106
+ Common features for commands oriented around an `SQLTags` database.
107
+ Subcommands:
108
+ dbinit [common-options...]
109
+ Initialise the database supporting `self.sqltags`.
110
+ This includes defining the schema and making the root metanode.
111
+ dbshell [common-options...]
112
+ Start an interactive database shell.
113
+ edit criteria...
114
+ Edit the entities specified by criteria.
115
+ export [common-options...] [-F format] [{tag[=value]|-tag}...]
116
+ Export entities matching all the constraints.
117
+ -F format Specify the export format, either CSV or FSTAGS.
118
+ find [common-options...] [-o output_format] {tag[=value]|-tag}...
119
+ List entities matching all the constraints.
120
+ -o output_format
121
+ Use output_format as a Python format string to lay out
122
+ the listing.
123
+ Default: {localtime} {headline}
124
+ help [common-options...] [-l] [-s] [subcommand-names...]
125
+ Print help for subcommands.
126
+ This outputs the full help for the named subcommands,
127
+ or the short help for all subcommands if no names are specified.
128
+ Options:
129
+ -l Long listing.
130
+ -r Recurse into subcommands.
131
+ -s Short listing.
132
+ import [common-options...] [{-u|--update}] {-|srcpath}...
133
+ Import CSV data in the format emitted by "export".
134
+ Each argument is a file path or "-", indicating standard input.
135
+ -u, --update If a named entity already exists then update its tags.
136
+ Otherwise this will be seen as a conflict
137
+ and the import aborted.
138
+ info [common-options...] [field-names...]
139
+ Recite general information.
140
+ Explicit field names may be provided to override the default listing.
141
+ log [common-options...] [-c category,...] [-d when] [-D strptime] {-|headline} [tags...]
142
+ Record entries into the database.
143
+ If headline is '-', read headlines from standard input.
144
+ Options:
145
+ -c categories Specify the categories for this log entry.
146
+ The default is to recognise a leading CAT,CAT,...: prefix.
147
+ -d dt Use dt, an ISO8601 date, as the log entry timestamp.
148
+ -D strptime-format Read the time from the start of the headline
149
+ according to the provided strptime specification.
150
+ orm [common-options...] define_schema
151
+ Runs the ORM's `define_schema()` method, which creates missing tables
152
+ and entity 0 if missing.
153
+ repl [common-options...]
154
+ Run a REPL (Read Evaluate Print Loop), an interactive Python prompt.
155
+ Options:
156
+ --banner banner Banner.
157
+ shell [common-options...]
158
+ Run a command prompt via cmd.Cmd using this command's subcommands.
159
+ tag [common-options...] {-|entity-name} {tag[=value]|-tag}...
160
+ Tag an entity with multiple tags.
161
+ With the form "-tag", remove that tag from the direct tags.
162
+ A entity-name named "-" indicates that entity-names should
163
+ be read from the standard input.
164
+
165
+ *`BaseSQLTagsCommand.Options`*
166
+
167
+ *`BaseSQLTagsCommand.parse_tagset_criterion(crit_s, tag_based_test_class=None)`*:
168
+ Parse a `TagSet` criterion from `crit_s`.
169
+
170
+ The criterion may be either:
171
+ * an integer specifying a `TagSet` id
172
+ * a tag criterion
173
+
174
+ *`BaseSQLTagsCommand.run_context(self)`*:
175
+ Prepare the `SQLTags` around each command invocation.
176
+ - <a name="glob2like"></a>`glob2like(glob: str) -> str`: Convert a filename glob to an SQL LIKE pattern.
177
+ - <a name="main"></a>`main(argv=None)`: Command line mode.
178
+ - <a name="PolyValue"></a>`class PolyValue(PolyValue, PolyValued)`: A `namedtuple` for the polyvalues used in an `SQLTagsORM`.
179
+
180
+ We express various types in SQL as one of 3 columns:
181
+ * `float_value`: for `float`s and `int`s which round trip with `float`
182
+ * `string_value`: for `str`
183
+ * `structured_value`: a JSON transcription of any other type
184
+
185
+ This allows SQL indexing of basic types.
186
+
187
+ Note that because `str` gets stored in `string_value`
188
+ this leaves us free to use "bare string" JSON to serialise
189
+ various nonJSONable types.
190
+
191
+ The `SQLTagSets` class has a `to_polyvalue` factory
192
+ which produces a `PolyValue` suitable for the SQL rows.
193
+ NonJSONable types such as `datetime`
194
+ are converted to a `str` but stored in the `structured_value` column.
195
+ This should be overridden by subclasses as necessary.
196
+
197
+ On retrieval from the database
198
+ the tag rows are converted to Python values
199
+ by the `SQLTagSets.from_polyvalue` method,
200
+ reversing the process above.
201
+ - <a name="PolyValueColumnMixin"></a>`class PolyValueColumnMixin(PolyValued)`: A mixin for classes with `(float_value,string_value,structured_value)` columns.
202
+ This is used by the `Tags` and `TagMultiValues` relations inside `SQLTagsORM`.
203
+ - <a name="PolyValued"></a>`class PolyValued`: A mixin for classes with `(float_value,string_value,structured_value)` columns.
204
+
205
+ *`PolyValued.as_polyvalue(self)`*:
206
+ Return this row's value as a `PolyValue`.
207
+
208
+ *`PolyValued.is_valid(self)`*:
209
+ Test that at most one attribute is non-`None`.
210
+
211
+ *`PolyValued.set_polyvalue(self, pv: 'PolyValued')`*:
212
+ Set all the value fields.
213
+
214
+ *`PolyValued.value_test(other_value)`*:
215
+ Return `(column,test_value)` for constructing tests against
216
+ `other_value` where `column` if the appropriate SQLAlchemy column
217
+ and `test_value` is the comparison value for testing.
218
+
219
+ For most `other_value`s the `test_value`
220
+ will just be `other_value`,
221
+ but for certain types the `test_value` will be:
222
+ * `NoneType`: `None`, and the column will also be `None`
223
+ * `datetime`: `datetime2unixtime(other_value)`
224
+ - <a name="prefix2like"></a>`prefix2like(prefix: str, esc='\\') -> str`: Convert a prefix string to an SQL LIKE pattern.
225
+ - <a name="SQLParameters"></a>`class SQLParameters(SQLParameters)`: The parameters required for constructing queries
226
+ or extending queries with JOINs.
227
+
228
+ Attributes:
229
+ * `criterion`: the source criterion, usually an `SQTCriterion` subinstance
230
+ * `alias`: an alias of the source table for use in queries
231
+ * `entity_id_column`: the `entities` id column,
232
+ `alias.id` if the alias is of `entities`,
233
+ `alias.entity_id` if the alias is of `tags`
234
+ * `constraint`: a filter query based on `alias`
235
+ - <a name="SQLTagBasedTest"></a>`class SQLTagBasedTest(cs.tagset.TagBasedTest, SQTCriterion)`: A `cs.tagset.TagBasedTest` extended with a `.sql_parameters` method.
236
+
237
+ *`SQLTagBasedTest.match_tagged_entity(self, te: cs.tagset.TagSet) -> bool`*:
238
+ Match this criterion against `te`.
239
+ - <a name="SQLTagProxies"></a>`class SQLTagProxies`: A proxy for the tags supporting Python comparison => `SQLParameters`.
240
+
241
+ Example:
242
+
243
+ sqltags.tags.dotted.name.here == 'foo'
244
+ - <a name="SQLTagProxy"></a>`class SQLTagProxy`: An object based on a `Tag` name
245
+ which produces an `SQLParameters` when compared with some value.
246
+
247
+ Example:
248
+
249
+ >>> sqltags = SQLTags('sqlite://')
250
+ >>> sqltags.init()
251
+ >>> # make a SQLParameters for testing the tag 'name.thing'==5
252
+ >>> sqlp = sqltags.tags.name.thing == 5
253
+ >>> str(sqlp.constraint)
254
+ 'tags_1.name = :name_1 AND tags_1.float_value = :float_value_1'
255
+ >>> sqlp = sqltags.tags.name.thing == 'foo'
256
+ >>> str(sqlp.constraint)
257
+ 'tags_1.name = :name_1 AND tags_1.string_value = :string_value_1'
258
+
259
+ *`SQLTagProxy.__eq__(self, other, alias=None) -> cs.sqltags.SQLParameters`*:
260
+ Return an SQL `=` test `SQLParameters`.
261
+
262
+ Example:
263
+
264
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing == 'foo'
265
+ >>> str(sqlp.constraint)
266
+ 'tags_1.name = :name_1 AND tags_1.string_value = :string_value_1'
267
+
268
+ *`SQLTagProxy.__ge__(self, other)`*:
269
+ Return an SQL `>=` test `SQLParameters`.
270
+
271
+ Example:
272
+
273
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing >= 'foo'
274
+ >>> str(sqlp.constraint)
275
+ 'tags_1.name = :name_1 AND tags_1.string_value >= :string_value_1'
276
+
277
+ *`SQLTagProxy.__getattr__(self, sub_tag_name)`*:
278
+ Magic access to dotted tag names: produce a new `SQLTagProxy` from ourself.
279
+
280
+ *`SQLTagProxy.__gt__(self, other)`*:
281
+ Return an SQL `>` test `SQLParameters`.
282
+
283
+ Example:
284
+
285
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing > 'foo'
286
+ >>> str(sqlp.constraint)
287
+ 'tags_1.name = :name_1 AND tags_1.string_value > :string_value_1'
288
+
289
+ *`SQLTagProxy.__le__(self, other)`*:
290
+ Return an SQL `<=` test `SQLParameters`.
291
+
292
+ Example:
293
+
294
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing <= 'foo'
295
+ >>> str(sqlp.constraint)
296
+ 'tags_1.name = :name_1 AND tags_1.string_value <= :string_value_1'
297
+
298
+ *`SQLTagProxy.__lt__(self, other)`*:
299
+ Return an SQL `<` test `SQLParameters`.
300
+
301
+ Example:
302
+
303
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing < 'foo'
304
+ >>> str(sqlp.constraint)
305
+ 'tags_1.name = :name_1 AND tags_1.string_value < :string_value_1'
306
+
307
+ *`SQLTagProxy.__ne__(self, other, alias=None) -> cs.sqltags.SQLParameters`*:
308
+ Return an SQL `<>` test `SQLParameters`.
309
+
310
+ Example:
311
+
312
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing != 'foo'
313
+ >>> str(sqlp.constraint)
314
+ 'tags_1.name = :name_1 AND tags_1.string_value != :string_value_1'
315
+
316
+ *`SQLTagProxy.by_op_text(self, op_text, other, alias=None)`*:
317
+ Return an `SQLParameters` based on the comparison's text representation.
318
+
319
+ Parameters:
320
+ * `op_text`: the comparsion operation text, one of:
321
+ `'='`, `'<='`, `'<'`, `'>='`, `'>'`, `'~'`.
322
+ * `other`: the other value for the comparison,
323
+ used to infer the SQL column name
324
+ and kept to provide the SQL value parameter
325
+ * `alias`: optional SQLAlchemy table alias
326
+
327
+ *`SQLTagProxy.likeglob(self, globptn: str) -> cs.sqltags.SQLParameters`*:
328
+ Return an SQL LIKE test approximating a glob as an `SQLParameters`.
329
+
330
+ Example:
331
+
332
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing.likeglob('foo*')
333
+ >>> str(sqlp.constraint)
334
+ "tags_1.name = :name_1 AND tags_1.string_value LIKE :string_value_1 ESCAPE '\\'"
335
+
336
+ *`SQLTagProxy.startswith(self, prefix: str) -> cs.sqltags.SQLParameters`*:
337
+ Return an SQL LIKE prefix test `SQLParameters`.
338
+
339
+ Example:
340
+
341
+ >>> sqlp = SQLTags('sqlite://').tags.name.thing.startswith('foo')
342
+ >>> str(sqlp.constraint)
343
+ "tags_1.name = :name_1 AND tags_1.string_value LIKE :string_value_1 ESCAPE '\\'"
344
+ - <a name="SQLTags"></a>`class SQLTags(cs.obj.SingletonMixin, cs.tagset.BaseTagSets, cs.deco.Promotable)`: A class using an SQL database to store its `TagSet`s.
345
+
346
+ *`SQLTags.TAGSETCLASS_DEFAULT(self, *a, _sqltags=None, **kw)`*:
347
+ Factory to return a suitable `TagSet` subclass instance.
348
+ This produces an `SQLTagSet` instance correctly associated with this `SQLTags`.
349
+
350
+ *`SQLTags.TagSetClass(self, *, name, **kw)`*:
351
+ Local implementation of `TagSetClass`
352
+ so that we can annotate it with a `.singleton_also_by` attribute.
353
+
354
+ *`SQLTags.__getitem__(self, index)`*:
355
+ Return an `SQLTagSet` for `index` (an `int` or `str`).
356
+
357
+ *`SQLTags.__setitem__(self, index, te)`*:
358
+ Dummy `__setitem__` which checks `te` against the db by type
359
+ because the factory has already inserted it into the database.
360
+
361
+ *`SQLTags.db_entity(self, index)`*:
362
+ Return the `Entities` instance for `index` or `None`.
363
+
364
+ *`SQLTags.db_session(self, *, new=False)`*:
365
+ Context manager to obtain a db session if required
366
+ (or if `new` is true).
367
+
368
+ *`SQLTags.dbshell(self)`*:
369
+ Run a database shell connected to the ORM database.
370
+
371
+ *`SQLTags.default_db_session`*:
372
+ The current per-`Thread` SQLAlchemy Session.
373
+
374
+ *`SQLTags.default_factory(self, name: Optional[str] = None, *, unixtime=None, tags=None)`*:
375
+ Fetch or create an `SQLTagSet` for `name`.
376
+ Return the `SQLTagSet`.
377
+
378
+ Note that `name` may be `None` to create a new "log" entry.
379
+
380
+ *`SQLTags.find(self, *criteria, _without_tags=False, **crit_kw)`*:
381
+ A generator to create and run a query derived from `criteria`,
382
+ yielding `SQLTagSet` instances.
383
+
384
+ Parameters:
385
+ * `criteria`: positional arguments which should be
386
+ `SQTCriterion`s or a `str` suitable for `SQTCriterion.from_str`
387
+ * `_without_tags`: optional flag to return entities without tags,
388
+ default `False`;
389
+ this can be used for a much faster scan of the entities
390
+ because it omits the `JOIN` against the tag table
391
+ * `crit_kw`: keyword parameters are appended to the criteria
392
+ as further tag equality tests
393
+
394
+ *`SQLTags.flush(self)`*:
395
+ Flush the current session state to the database.
396
+
397
+ *`SQLTags.from_str(db_url: str)`*:
398
+ Create an `SQLTags` from a `db_url` string.
399
+
400
+ *`SQLTags.get(self, index, default=None)`*:
401
+ Return an `SQLTagSet` matching `index`, or `default` if there is no such entity.
402
+
403
+ *`SQLTags.import_csv_file(self, f, *, update_mode=False)`*:
404
+ Import CSV data from the file `f`.
405
+
406
+ If `update_mode` is true
407
+ named records which already exist will update from the data,
408
+ otherwise the conflict will raise a `ValueError`.
409
+
410
+ *`SQLTags.import_tagged_entity(self, te, *, update_mode=False) -> None`*:
411
+ Import the `TagSet` `te`.
412
+
413
+ This updates the database with the contents of the supplied `TagSet`,
414
+ which has no inherent relationship to the database.
415
+
416
+ If `update_mode` is true
417
+ named records which already exist will update from `te`,
418
+ otherwise the conflict will raise a `ValueError`.
419
+
420
+ *`SQLTags.infer_db_url(envvar=None, default_path=None)`*:
421
+ Infer the database URL.
422
+
423
+ Parameters:
424
+ * `envvar`: environment variable to specify a default,
425
+ default from `cls.DBURL_ENVVAR` i.e. from `$SQLTAGS_DBURL`.
426
+ * `default_path`: optional default db URL if no environment
427
+ variable, default from `cls.DBURL_DEFAULT` (`~/var/sqltags.sqlite`).
428
+
429
+ *`SQLTags.items(self, *, prefix=None) -> Iterable[Tuple[str, cs.sqltags.SQLTagSet]]`*:
430
+ Return an iterable of `(tagset_name,TagSet)`.
431
+ Excludes unnamed `TagSet`s.
432
+
433
+ Constrain the names to those starting with `prefix`
434
+ if `prefix` is not `None`.
435
+
436
+ *`SQLTags.keys(self, *, prefix=None)`*:
437
+ Yield all the nonNULL names.
438
+
439
+ Constrain the names to those starting with `prefix`
440
+ if not `None`.
441
+
442
+ *`SQLTags.metanode`*:
443
+ The metadata node.
444
+
445
+ *`SQLTags.preload(self, *criteria) -> Set[cs.sqltags.SQLTagSet]`*:
446
+ Preload the `SQLTagSets` matching each criterion in `criteria`.
447
+ Note that is is effectively an OR of the `criteria`, not an AND.
448
+ Return a `set` of the entities.
449
+
450
+ *`SQLTags.startup_shutdown(self)`*:
451
+ Open the ORM while the `SQLTags` is open.
452
+
453
+ *`SQLTags.values(self, *, prefix=None)`*:
454
+ Return an iterable of the named `TagSet`s.
455
+ Excludes unnamed `TagSet`s.
456
+
457
+ Constrain the names to those starting with `prefix`
458
+ if not `None`.
459
+ - <a name="SQLTagsCommand"></a>`class SQLTagsCommand(BaseSQLTagsCommand)`: `sqltags` main command line utility.
460
+
461
+ Usage summary:
462
+
463
+ Usage: sqltags [common-options...] subcommand [options...]
464
+ Common features for commands oriented around an `SQLTags` database.
465
+ Subcommands:
466
+ dbinit [common-options...]
467
+ Initialise the database supporting `self.sqltags`.
468
+ This includes defining the schema and making the root metanode.
469
+ dbshell [common-options...]
470
+ Start an interactive database shell.
471
+ edit criteria...
472
+ Edit the entities specified by criteria.
473
+ export [common-options...] [-F format] [{tag[=value]|-tag}...]
474
+ Export entities matching all the constraints.
475
+ -F format Specify the export format, either CSV or FSTAGS.
476
+ find [common-options...] [-o output_format] {tag[=value]|-tag}...
477
+ List entities matching all the constraints.
478
+ -o output_format
479
+ Use output_format as a Python format string to lay out
480
+ the listing.
481
+ Default: {localtime} {headline}
482
+ help [common-options...] [-l] [-s] [subcommand-names...]
483
+ Print help for subcommands.
484
+ This outputs the full help for the named subcommands,
485
+ or the short help for all subcommands if no names are specified.
486
+ Options:
487
+ -l Long listing.
488
+ -r Recurse into subcommands.
489
+ -s Short listing.
490
+ import [common-options...] [{-u|--update}] {-|srcpath}...
491
+ Import CSV data in the format emitted by "export".
492
+ Each argument is a file path or "-", indicating standard input.
493
+ -u, --update If a named entity already exists then update its tags.
494
+ Otherwise this will be seen as a conflict
495
+ and the import aborted.
496
+ info [common-options...] [field-names...]
497
+ Recite general information.
498
+ Explicit field names may be provided to override the default listing.
499
+ list [common-options...] [entity-names...]
500
+ List entities and their tags.
501
+ Options:
502
+ -l Long mode.
503
+ log [common-options...] [-c category,...] [-d when] [-D strptime] {-|headline} [tags...]
504
+ Record entries into the database.
505
+ If headline is '-', read headlines from standard input.
506
+ Options:
507
+ -c categories Specify the categories for this log entry.
508
+ The default is to recognise a leading CAT,CAT,...: prefix.
509
+ -d dt Use dt, an ISO8601 date, as the log entry timestamp.
510
+ -D strptime-format Read the time from the start of the headline
511
+ according to the provided strptime specification.
512
+ ls [common-options...] [entity-names...]
513
+ List entities and their tags.
514
+ Options:
515
+ -l Long mode.
516
+ orm [common-options...] define_schema
517
+ Runs the ORM's `define_schema()` method, which creates missing tables
518
+ and entity 0 if missing.
519
+ repl [common-options...]
520
+ Run a REPL (Read Evaluate Print Loop), an interactive Python prompt.
521
+ Options:
522
+ --banner banner Banner.
523
+ shell [common-options...]
524
+ Run a command prompt via cmd.Cmd using this command's subcommands.
525
+ tag [common-options...] {-|entity-name} {tag[=value]|-tag}...
526
+ Tag an entity with multiple tags.
527
+ With the form "-tag", remove that tag from the direct tags.
528
+ A entity-name named "-" indicates that entity-names should
529
+ be read from the standard input.
530
+
531
+ *`SQLTagsCommand.cmd_list(self, argv)`*:
532
+ Usage: {cmd} [entity-names...]
533
+ List entities and their tags.
534
+ Options:
535
+ -l Long mode.
536
+
537
+ *`SQLTagsCommand.cmd_ls(self, argv)`*:
538
+ Usage: {cmd} [entity-names...]
539
+ List entities and their tags.
540
+ Options:
541
+ -l Long mode.
542
+ - <a name="SQLTagSet"></a>`class SQLTagSet(cs.obj.SingletonMixin, cs.tagset.TagSet)`: A singleton `TagSet` attached to an `SQLTags` instance.
543
+
544
+ As with the `TagSet` superclass,
545
+ tag values can be any Python type.
546
+ However, because we are storing these values in an SQL database
547
+ it is necessary to provide a conversion facility
548
+ to prepare those values for storage.
549
+
550
+ The database schema is described in the `SQLTagsORM` class;
551
+ in short we directly support `None`, `float` and `str`,
552
+ `int`s which round trip with `float`,
553
+ and `list`, `tuple` and `dict` whose contents transcribe to JSON.
554
+
555
+ `int`s which are too large to round trip with `float`
556
+ are treated as an extended `"bigint"` type
557
+ using the scheme described below.
558
+
559
+ Because the ORM has distinct `float` and `str` columns to support indexing,
560
+ there will be no plain strings in the remaining JSON blob column.
561
+ Therefore we support other types by providing functions
562
+ to convert each type to a `str` and back,
563
+ and an associated "type label" which will be prefixed to the string;
564
+ the resulting string is stored in the JSON blob.
565
+
566
+ The default mechanism is based on the following class attributes and methods:
567
+ * `TYPE_JS_MAPPING`: a mapping of a type label string
568
+ to a 3 tuple of `(type,to_str,from_str)`
569
+ being the extended type,
570
+ a function to convert an instance to `str`
571
+ and a function to convert a `str` to an instance of this type
572
+ * `to_js_str`: a method accepting `(tag_name,tag_value)`
573
+ and returning `tag_value` as a `str`;
574
+ the default implementation looks up the type of `tag_value`
575
+ in `TYPE_JS_MAPPING` to locate the corresponding `to_str` function
576
+ * `from_js_str`: a method accepting `(tag_name,js)`
577
+ which uses the leading type label prefix from the `js`
578
+ to look up the corresponding `from_str` function
579
+ from `TYPE_JS_MAPPING` and use it on the tail of `js`
580
+
581
+ The default `TYPE_JS_MAPPING` has mappings for:
582
+ * `"bigint"`: conversions for `int`
583
+ * `"date"`: conversions for `datetime.date`
584
+ * `"datetime"`: conversions for `datetime.datetime`
585
+
586
+ Subclasses wanting to augument the `TYPE_JS_MAPPING`
587
+ should prepare their own with code such as:
588
+
589
+ class SubSQLTagSet(SQLTagSet,....):
590
+ ....
591
+ TYPE_JS_MAPPING=dict(SQLTagSet.TYPE_JS_MAPPING)
592
+ TYPE_JS_MAPPING.update(
593
+ typelabel=(type, to_str, from_str),
594
+ ....
595
+ )
596
+
597
+ *`SQLTagSet.add_db_tag(self, tag_name, pv: cs.sqltags.PolyValue)`*:
598
+ Add a tag to the database.
599
+
600
+ *`SQLTagSet.child_tagsets(self, tag_name='parent')`*:
601
+ Return the child `TagSet`s as defined by their parent `Tag`,
602
+ by default the `Tag` named `'parent'`.
603
+
604
+ *`SQLTagSet.db_session(self, new=False)`*:
605
+ Context manager to obtain a new session if required,
606
+ just a shim for `self.sqltags.db_session`.
607
+
608
+ *`SQLTagSet.discard_db_tag(self, tag_name: str, pv: Optional[cs.sqltags.PolyValue] = None)`*:
609
+ Discard a tag from the database.
610
+
611
+ *`SQLTagSet.from_js_str(tag_name: str, js: str)`*:
612
+ Convert the `str` `js` to a `Tag` value.
613
+ This is the reverse of `as_js_str`.
614
+
615
+ Subclasses wanting extra type support
616
+ should either:
617
+ (usual approach) provide their own `TYPE_JS_MAPPING` class attribute
618
+ as described at the top of this class
619
+ or (for unusual requirements) override this method and also `to_js_str`.
620
+
621
+ *`SQLTagSet.from_polyvalue(tag_name: str, pv: cs.sqltags.PolyValue)`*:
622
+ Convert an SQL `PolyValue` to a tag value.
623
+
624
+ This can be overridden by subclasses along with `to_polyvalue`.
625
+ The `tag_name` is provided for context
626
+ in case it should influence the normalisation.
627
+
628
+ *`SQLTagSet.id`*:
629
+ The `.id` aka `self._id`.
630
+
631
+ *`SQLTagSet.jsonable(value)`*:
632
+ Convert `value` to a form which can be directly JSON serialised.
633
+
634
+ In particular this converts non-list/set/tuple Sequences to lists
635
+ and non-dict Mappings to dicts.
636
+
637
+ Warning: this is not robust against cycles.
638
+
639
+ *`SQLTagSet.name`*:
640
+ The `.name` aka `self._name`.
641
+
642
+ *`SQLTagSet.parent_tagset(self, tag_name='parent')`*:
643
+ Return the parent `TagSet` as defined by a `Tag`,
644
+ by default the `Tag` named `'parent'`.
645
+
646
+ *`SQLTagSet.setdefault(self, tag_name, value)`*:
647
+ Return `self[tag_name]`, setting it to `value` if not already present.
648
+
649
+ *`SQLTagSet.to_js_str(tag_name: str, tag_value) -> str`*:
650
+ Convert `tag_value` to a `str` suitable for storage in `structure_value`.
651
+ This can be reversed by `from_js_str`.
652
+
653
+ Subclasses wanting extra type support
654
+ should either:
655
+ (usual approach) provide their own `TYPE_JS_MAPPING` class attribute
656
+ as described at the top of this class
657
+ or (for unusual requirements) override this method and also `from_js_str`.
658
+
659
+ *`SQLTagSet.to_polyvalue(tag_name: str, tag_value) -> cs.sqltags.PolyValue`*:
660
+ Normalise `Tag` values for storage via SQL.
661
+ Preserve things directly expressable in JSON.
662
+ Convert other values via `to_js_str`.
663
+ Return a `PolyValue` for use with the SQL rows.
664
+ - <a name="SQLTagsORM"></a>`class SQLTagsORM(cs.sqlalchemy_utils.ORM, cs.dateutils.UNIXTimeMixin)`: The ORM for an `SQLTags`.
665
+
666
+ The current implementation uses 3 tables:
667
+ * `entities`: this has a NULLable `name`
668
+ and `unixtime` UNIX timestamp;
669
+ this is unique per `name` if the name is not NULL
670
+ * `tags`: this has an `entity_id`, `name` and a value stored
671
+ in one of three columns: `float_value`, `string_value` and
672
+ `structured_value` which is a JSON blob;
673
+ this is unique per `(entity_id,name)`
674
+ * `tag_subvalues`: this is a broken out version of `tags`
675
+ when `structured_value` is a sequence or mapping,
676
+ breaking out the values one per row;
677
+ this exists to support "tag contains value" lookups
678
+
679
+ Tag values are stored as follows:
680
+ * `None`: all 3 columns are set to `NULL`
681
+ * `float`: stored in `float_value`
682
+ * `int`: if the `int` round trips to `float`
683
+ then it is stored in `float_value`,
684
+ otherwise it is stored in `structured_value`
685
+ with the type label `"bigint"`
686
+ * `str`: stored in `string_value`
687
+ * `list`, `tuple`, `dict`: stored in `structured_value`;
688
+ if these containers contain unJSONable content there will be trouble
689
+ * other types, such as `datetime`:
690
+ these are converted to strings with identifying type label prefixes
691
+ and stored in `structured_value`
692
+
693
+ The `float_value` and `string_value` columns
694
+ allow us to provide indices for these kinds of tag values.
695
+
696
+ The type label scheme takes advantage of the fact that actual `str`s
697
+ are stored in the `string_value` column.
698
+ Because of this, there will be no actual strings in `structured_value`.
699
+ Therefore, we can convert nonJSONable types to `str` and store them here.
700
+
701
+ The scheme used is to provide conversion functions to convert types
702
+ to `str` and back, and an associated "type label" prefix.
703
+ For example, we store a `datetime` as the ISO format of the `datetime`
704
+ with `"datetime:"` prefixed to it.
705
+
706
+ The actual conversions are kept with the `SQLTagSet` class
707
+ (or any subclass).
708
+ This ORM receives the 3-tuples of SQL ready values
709
+ from that class as the `PolyValue` `namedtuple`
710
+ and does not perform any conversion itself.
711
+ The conversion process is described in `SQLTagSet`.
712
+
713
+ *`SQLTagsORM.declare_schema(self)`*:
714
+ Define the database schema / ORM mapping.
715
+
716
+ *`SQLTagsORM.prepare_metanode(self, *, session)`*:
717
+ Ensure row id 0, the metanode, exists.
718
+
719
+ *`SQLTagsORM.search(self, criteria, *, session, mode='tagged')`*:
720
+ Construct a query to match `Entity` rows
721
+ matching the supplied `criteria` iterable.
722
+ Return an SQLAlchemy `Query`.
723
+
724
+ The `mode` parameter has the following values:
725
+ * `'id'`: the query only yields entity ids
726
+ * `'entity'`: (default) the query yields entities without tags
727
+ * `'tagged'`: (default) the query yields entities left outer
728
+ joined with their matching tags
729
+
730
+ Note that the `'tagged'` result produces multiple rows for any
731
+ entity with multiple tags, and that this requires the caller to
732
+ fold entities with multiple tags together.
733
+
734
+ *Note*:
735
+ due to implementation limitations
736
+ the SQL query itself may not apply all the criteria,
737
+ so every criterion must still be applied
738
+ to the results
739
+ using its `.match_entity` method.
740
+
741
+ If `name` is omitted or `None` the query will match log entities
742
+ otherwise the entity with the specified `name`.
743
+
744
+ The `criteria` should be an iterable of `SQTCriterion` instances
745
+ used to construct the query.
746
+ - <a name="SQTCriterion"></a>`class SQTCriterion(cs.tagset.TagSetCriterion)`: Subclass of `TagSetCriterion` requiring an `.sql_parameters` method
747
+ which returns an `SQLParameters` providing the information required
748
+ to construct an sqlalchemy query.
749
+ It also resets `.CRITERION_PARSE_CLASSES`, which will pick up
750
+ the SQL capable criterion classes below.
751
+
752
+ *`SQTCriterion.TAG_BASED_TEST_CLASS`*
753
+
754
+ *`SQTCriterion.from_equality(tag_name, tag_value)`*:
755
+ Return an `SQTCriterion` instance based on `tag_name==tag_value`.
756
+ This supports `SQLTags.find`'s keyword parameters.
757
+
758
+ *`SQTCriterion.match_tagged_entity(self, te: cs.tagset.TagSet) -> bool`*:
759
+ Perform the criterion test on the Python object directly.
760
+ This is used at the end of a query to implement tests which
761
+ cannot be sufficiently implemented in SQL.
762
+ If `self.SQL_COMPLETE` it is not necessary to call this method.
763
+
764
+ *`SQTCriterion.sql_parameters(self, orm) -> cs.sqltags.SQLParameters`*:
765
+ Subclasses must return am `SQLParameters` instance
766
+ parameterising the SQL queries that follow.
767
+ - <a name="SQTEntityIdTest"></a>`class SQTEntityIdTest(SQTCriterion)`: A test on `entity.id`.
768
+
769
+ *`SQTEntityIdTest.match_tagged_entity(self, te: cs.tagset.TagSet) -> bool`*:
770
+ Test the `TagSet` `te` against `self.entity_ids`.
771
+
772
+ *`SQTEntityIdTest.parse(s, offset=0, delim=None)`*:
773
+ Parse a decimal entity id from `s`.
774
+ - <a name="UsesSQLTags"></a>`class UsesSQLTags(cs.tagset.UsesTagSets)`: A mixin subclassing `UsesTagSets` to support classes which
775
+ use an `SQLTags` to store their data.
776
+
777
+ As with `cs.tagset.UsesTagSets`, subclasses must supply a
778
+ `hastags_class` and _may_ supply a `tagsets_class` if it
779
+ should not be `SQLTags`. The subclass must also define a
780
+ `.TYPE_ZONE` class attribute.
781
+ An example from `cs.cdrip`:
782
+
783
+ class MBDB(UsesSQLTags, MultiOpenMixin, RunStateMixin:
784
+
785
+ TYPE_ZONE = 'mbdb'
786
+ HasTagsClass = _MBEntity
787
+ TagSetsClass = MBSQLTags
788
+
789
+ *`UsesSQLTags.TagSetsClass`*
790
+ - <a name="verbose"></a>`verbose(msg, *a)`: Emit message if in verbose mode.
791
+
792
+ # Release Log
793
+
794
+
795
+
796
+ *Release 20260531*:
797
+ * BaseSQLTagsCommand: modernise usage spec and common options, make .sqltags a cached property of BaseSQLTagsCommand (uses self.TAGSETS_CLASS, hence not in the Options).
798
+ * SQLTagSet: provide __lt__ to order entities.
799
+ * SQLTags now subclasses SingletonMixin, keying on the normalised db URL for filesystem paths.
800
+ * SQLTagSet.jsonable: turn datetimes into ISO8601 strings.
801
+ * SQLTags.__getitem__: initial support for a (tag_name,value) 2-tuple as an index.
802
+ * SQLTags.__getitem__: if the (missing) index is a (name,value) 2-tuple create the entity with name=None and set the tag.
803
+ * SQLTags.__getitem__: for a (tag_name,tag_value) 2-tuple, if the tag_name is "name" create it with that name.
804
+ * SQLTagSet.TYPE_JS_MAPPING: add UUID to the supported types.
805
+ * SQLTagBasedTest.sql_parameters: support name=None.
806
+ * SQLTagSet: new .deref() method to dereference tags whose values refer to other SQLTagSets.
807
+ * SQLTagsCommandsMixin: rename cmd_init to cmd_dbinit.
808
+ * New HasSQLTags(HasTags) mixin providing a .tags_db proptery from self.tags.sqltags.
809
+ * New UsesSQLTags mixin providing .__getitem__(type_subname,key).
810
+ * cs.sqltags,cs.cdrip: move the MBDB.find method into UsesSQLTags.
811
+ * Drop HasSQLTags (HasTags does it all now), make UsesSQLTags subclass UsesTagSets and just set TagSetsClass=SQLTags.
812
+ * New SQLTags.dbshell() method to run an interactive db prompt.
813
+ * SQLTags: new .preload(*criteria) method to preload and return TagSets.
814
+ * tagset,fstags,sqltags: make _id only special to SQLTagSet (the db row id), drop all mentions elsewhere.
815
+ * SQLTagSet.set: new optional force=False parameter, skips db update if false and value unchanged.
816
+ * SQLTags.default_factory: drop skip_refresh support, will be covered by the Refreshable stuff.
817
+
818
+ *Release 20240723*:
819
+ * Replace many raises of RuntimeError with NotImplementedError, suggestion by @dimaqq on discuss.python.org.
820
+ * Move some constants from BaseSQLTagsCommand to SQLTagsCommandsMixin where they belong, add missing USAGE_KEYWORDS entry.
821
+ * SQLTagsORM: do not define_schema() in init, instead let the ORM do that and we prepare the metanode on first use of the db - this makes it much cheaper to make an SQLTags and then not use it.
822
+
823
+ *Release 20240316*:
824
+ Fixed release upload artifacts.
825
+
826
+ *Release 20240305*:
827
+ SQLTags: new .from_str so that we can inherit Promotable.promote.
828
+
829
+ *Release 20240201.1*:
830
+ Release with the "sqltags" script.
831
+
832
+ *Release 20240201*:
833
+ * SQLTagSet.to_polyvalue: treat sets like lists.
834
+ * SQLTags.default_factory: honour new skip_refresh parameter, apply any presupplied tags.
835
+ * Pull the cmd_* methods from BaseSQLTagsCommand into new SQLTagsCommandsMixin for reuse.
836
+
837
+ *Release 20230612*:
838
+ * SQLTagBasedTest.sql_parameters: fix general tag name.
839
+ * SQLTagSet: new jsonable class method to produce a JSON serialisable object - converts sets and Sequences to flat lists, etc.
840
+
841
+ *Release 20230217*:
842
+ SQLTagsORM.search: previous changes seem to have dropped SQTCriterion support.
843
+
844
+ *Release 20230212.1*:
845
+ Mark SQLTags as promotable.
846
+
847
+ *Release 20230212*:
848
+ * @promote support for SQLTags, promoting a filesystem path to a .sqlite db.
849
+ * Simpler SQLTagsORM.search comparison implementation.
850
+ * SQLTagSet: inherit format attributes from superclasses (TagSet).
851
+ * New BaseSQLTagsCommand.cmd_shell method.
852
+ * New BaseSQLTagsCommand.cmd_orm method with "define_schema" subcommand to update the db schema.
853
+ * SQLTagsORM.__init__: drop case_sensitive, no longer supported?
854
+ * SQLTagsORM.__init__: always call define_schema, it seems there are scenarios where this does some necessary sqlalchemy prep.
855
+
856
+ *Release 20221228*:
857
+ SQLTagsCommand: update implementation of BaseCommand.run_context to use super().run_context().
858
+
859
+ *Release 20220806*:
860
+ * Bugfix for SQLTagsORM.search(mode='entity').
861
+ * SQLTags.find: new _without_tags=False parameter to allow fast searches omitting the entity tags.
862
+
863
+ *Release 20220606*:
864
+ * New SQLTagsORM.Entities.add_new_tags method, use it in SQLTags.default_factory for bulk insert.
865
+ * SQTCriterion: new .from_equality(tag_name,tag_value) factory to make an equality criterion.
866
+ * SQLTags.find: accept criteria as positional parameters instead of a single iterable, accept new keyword parameters as equality criteria.
867
+ * SQLTags.__getitem__: accept a slice to index the .unixtime tag.
868
+ * SQLTagsORM: also turn on echo mode if "ECHO" in $SQLTAGS_MODES.
869
+
870
+ *Release 20220311*:
871
+ Assorted updates.
872
+
873
+ *Release 20211212*:
874
+ * Rename edit_many to edit_tagsets for clarity.
875
+ * Small bugfixes.
876
+
877
+ *Release 20210913*:
878
+ * SQLTagsCommand: rename cmd_ns to cmd_list,cmd_ls.
879
+ * SQLTagsCommand.cmd_export: accept "-F export_format" for csv or fstags export, accept no criteria to mean all tagsets.
880
+ * Encoding schema for nonJSONable types.
881
+ * Rename the TagSets abstract base class to BaseTagSets.
882
+ * BaseSQLTagsCommand.cmd_edit: implement rename.
883
+ * Many other internal small changes.
884
+
885
+ *Release 20210420*:
886
+ * New PolyValueMixin pulled out of Tags for common support of the (float_value,string_value,structured_value).
887
+ * SQLTagsORM: new TagSubValues relation containing broken out values for values which are sequences, to support efficient lookup if sequence values such as log entry categories.
888
+ * New BaseSQLTagsCommand.parse_categories static method to parse FOO,BAH into ['foo','bah'].
889
+ * sqltags find: change default format to "{datetime} {headline}".
890
+ * Assorted small changes.
891
+
892
+ *Release 20210404*:
893
+ * SQLTags.__getitem__: when autocreating an entity, do it in a new session so that the entity is commited to the database before any further use.
894
+ * SQLTagsCommand: new cmd_dbshell to drop you into the database.
895
+
896
+ *Release 20210321*:
897
+ Drop logic now merged with cs.sqlalchemy_utils, use the new default session stuff.
898
+
899
+ *Release 20210306.1*:
900
+ Docstring updates.
901
+
902
+ *Release 20210306*:
903
+ Initial release.