context-fabric 0.1.0__py3-none-any.whl

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.
cfabric/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Context-Fabric package
@@ -0,0 +1,25 @@
1
+ """
2
+ Core API of Context-Fabric.
3
+
4
+ This API deals with the basic CF data model: a graph of nodes and edges,
5
+ annotated by features.
6
+
7
+ The core API consists of:
8
+
9
+ * `N`: see `nodes.Nodes` (walk through nodes)
10
+ * `F`: see `nodefeature.NodeFeature` (retrieve feature values for nodes)
11
+ * `E`: see `edgefeature.EdgeFeature` (retrieve feature values for edges)
12
+ * `L`: see `locality.Locality` (move between levels)
13
+ * `T`: see `text.Text` (get the text)
14
+ * `S`: see `search.search` (search by templates)
15
+
16
+ plus some additional methods.
17
+
18
+ Based on Text-Fabric by Dirk Roorda.
19
+ """
20
+
21
+ from cfabric.core.fabric import Fabric
22
+ from cfabric.core.parameters import VERSION, NAME, BANNER
23
+
24
+ __version__ = VERSION
25
+ __all__ = ["Fabric", "VERSION", "NAME", "BANNER"]
cfabric/core/api.py ADDED
@@ -0,0 +1,586 @@
1
+ """
2
+ # The core API of TF.
3
+
4
+ It provides methods to navigate nodes and edges and lookup features.
5
+ """
6
+
7
+ import collections
8
+ from textwrap import wrap, dedent
9
+
10
+ from cfabric.core.helpers import flattenToSet, console, fitemize, deepSize
11
+ from cfabric.core.files import unexpanduser as ux
12
+ from cfabric.core.nodes import Nodes
13
+ from cfabric.core.locality import Locality
14
+ from cfabric.core.nodefeature import NodeFeatures
15
+ from cfabric.core.edgefeature import EdgeFeatures
16
+ from cfabric.core.computed import Computeds
17
+ from cfabric.core.text import Text
18
+ from cfabric.core.parameters import OTYPE, OSLOTS
19
+ from cfabric.core.search.search import Search
20
+ from cfabric.core.timestamp import SILENT_D, DEEP, silentConvert
21
+
22
+
23
+ API_REFS = dict(
24
+ AllComputeds=("Computed", "computedall", "computed-data"),
25
+ AllEdges=("Features", "edgeall", "edge-features"),
26
+ AllFeatures=("Features", "nodeall", "node-features"),
27
+ C=("Computed", "computed", "computed-data"),
28
+ Call=("Computed", "computedall", "computed-data"),
29
+ Computed=("Computed", "computed", "computed-data"),
30
+ ComputedString=("Computed", "computedstr", "computed-data"),
31
+ Cs=("Computed", "computedstr", "computed-data"),
32
+ E=("Features", "edge", "edge-features"),
33
+ Eall=("Features", "edgeall", "edge-features"),
34
+ Edge=("Features", "edge", "edge-features"),
35
+ EdgeString=("Features", "edgestr", "edge-features"),
36
+ Es=("Features", "edgestr", "edge-features"),
37
+ F=("Features", "node", "node-features"),
38
+ Fall=("Features", "nodeall", "node-features"),
39
+ Feature=("Features", "node", "node-features"),
40
+ FeatureString=("Features", "nodestr", "node-features"),
41
+ Fs=("Features", "nodestr", "node-features"),
42
+ L=("Locality", "locality", "locality"),
43
+ Locality=("Locality", "locality", "locality"),
44
+ N=("Nodes", "nodes", "navigating-nodes"),
45
+ Nodes=("Nodes", "nodes", "navigating-nodes"),
46
+ S=("Search", "search", "search"),
47
+ Search=("Search", "search", "search"),
48
+ T=("Text", "text", "text"),
49
+ TF=("Fabric", "fabric", "loading"),
50
+ Text=("Text", "text", "text"),
51
+ )
52
+
53
+
54
+ class Api:
55
+ def __init__(self, TF):
56
+ self.TF = TF
57
+ self.ignored = tuple(sorted(TF.featuresIgnored))
58
+ """Which features were found but ignored.
59
+
60
+ Features are ignored if the feature is also present in another location
61
+ that is loaded later.
62
+ """
63
+ TF.ignored = self.ignored
64
+
65
+ self.F = NodeFeatures()
66
+ self.Feature = self.F
67
+ self.E = EdgeFeatures()
68
+ self.Edge = self.E
69
+ self.C = Computeds()
70
+ self.Computed = self.C
71
+ tmObj = TF.tmObj
72
+ TF.silentOn = tmObj.silentOn
73
+ TF.silentOff = tmObj.silentOff
74
+ TF.isSilent = tmObj.isSilent
75
+ TF.setSilent = tmObj.setSilent
76
+ TF.info = tmObj.info
77
+ TF.warning = tmObj.warning
78
+ TF.error = tmObj.error
79
+ TF.cache = tmObj.cache
80
+ TF.reset = tmObj.reset
81
+ TF.indent = tmObj.indent
82
+
83
+ """All messages produced during the feature loading process.
84
+
85
+ It also shows the messages that have been suppressed due to the `silent`
86
+ parameter.
87
+ """
88
+
89
+ TF.ensureLoaded = self.ensureLoaded
90
+ TF.makeAvailableIn = self.makeAvailableIn
91
+ TF.footprint = self.footprint
92
+
93
+ setattr(self, "FeatureString", self.Fs)
94
+ setattr(self, "EdgeString", self.Es)
95
+ setattr(self, "ComputedString", self.Cs)
96
+ setattr(self, "AllFeatures", self.Fall)
97
+ setattr(self, "AllEdges", self.Eall)
98
+ setattr(self, "AllComputeds", self.Call)
99
+ setattr(self, "loadLog", self.isLoaded)
100
+
101
+ def Fs(self, fName, warn=True):
102
+ """Get the node feature sub API.
103
+
104
+ If feature name is not a valid python identifier,
105
+ or if you do not know its name in advance,
106
+ you can not use `F.feature`, but you should use
107
+ `Fs(feature)`.
108
+
109
+ Parameters
110
+ ----------
111
+ fName: string
112
+ The name of the feature.
113
+ warn: boolean, optional `True`
114
+ Whether to warn if the feature is not loaded.
115
+
116
+ Returns
117
+ -------
118
+ The feature API, or `None` if the feature is not loaded.
119
+ """
120
+
121
+ if not hasattr(self.F, fName):
122
+ if warn:
123
+ self.TF.error(f'Node feature "{fName}" not loaded')
124
+ return None
125
+ return getattr(self.F, fName)
126
+
127
+ def Es(self, fName, warn=True):
128
+ """Get the edge feature sub API.
129
+
130
+ If feature name is not a valid python identifier,
131
+ or if you do not know its name in advance,
132
+ you can not use `E.feature`, but you should use
133
+ `Es(feature)`.
134
+
135
+ Parameters
136
+ ----------
137
+ fName: string
138
+ The name of the feature.
139
+ warn: boolean, optional `True`
140
+ Whether to warn if the feature is not loaded.
141
+
142
+ Returns
143
+ -------
144
+ The feature API, or `None` if the feature is not loaded.
145
+ """
146
+
147
+ if not hasattr(self.E, fName):
148
+ if warn:
149
+ self.TF.error(f'Edge feature "{fName}" not loaded')
150
+ return None
151
+ return getattr(self.E, fName)
152
+
153
+ def Cs(self, fName, warn=True):
154
+ """Get the computed data sub API.
155
+
156
+ If component name is not a valid python identifier,
157
+ or if you do not know its name in advance,
158
+ you can not use `C.component`, but you should use
159
+ `Cs(component)`.
160
+
161
+ Parameters
162
+ ----------
163
+ fName: string
164
+ The name of the feature.
165
+ warn: boolean, optional `True`
166
+ Whether to warn if the feature is not loaded.
167
+
168
+ Returns
169
+ -------
170
+ The feature API, or `None` if the feature is not loaded.
171
+ """
172
+
173
+ if not hasattr(self.C, fName):
174
+ if warn:
175
+ self.TF.error(f'Computed feature "{fName}" not loaded')
176
+ return None
177
+ return getattr(self.C, fName)
178
+
179
+ def Fall(self, warp=True):
180
+ """Returns a sorted list of all usable, loaded node feature names.
181
+
182
+ Parameters
183
+ ----------
184
+ warp: boolean, optional True
185
+ Whether to include the warp features, i.e. `otype`.
186
+ """
187
+
188
+ return sorted(x[0] for x in self.F.__dict__.items() if warp or x[0] != OTYPE)
189
+
190
+ def Eall(self, warp=True):
191
+ """Returns a sorted list of all usable, loaded edge feature names.
192
+
193
+ Parameters
194
+ ----------
195
+ warp: boolean, optional True
196
+ Whether to include the warp features, i.e. `oslots`.
197
+ """
198
+
199
+ return sorted(x[0] for x in self.E.__dict__.items() if warp or x[0] != OSLOTS)
200
+
201
+ def Call(self):
202
+ """Returns a sorted list of all usable, loaded computed data names."""
203
+
204
+ return sorted(x[0] for x in self.C.__dict__.items())
205
+
206
+ def isLoaded(
207
+ self, features=None, pretty=True, valueType=True, path=False, meta="description"
208
+ ):
209
+ """Show information about loaded features.
210
+
211
+ Parameters
212
+ ----------
213
+ features: iterable | string, optional None
214
+ The features to get info for.
215
+ If absent or None: all features seen by TF.
216
+ If a string, it is a comma and / or space separated list of feature names.
217
+ Otherwise the items of the iterable are feature names.
218
+
219
+ pretty: boolean, optional True
220
+ If True, it prints an overview of all features seen by TF with
221
+ information about kind, type, source location and loaded status.
222
+ The amount of information printed can be tweaked by other parameters.
223
+ Otherwise, it returns this information as a dict.
224
+
225
+ valueType: boolean, optional True
226
+ Only relevant if `pretty=True`: whether to print the value type of
227
+ the values in the feature file.
228
+
229
+ path: boolean, optional True
230
+ Only relevant if `pretty=True`: whether to print the path name of
231
+ the feature file.
232
+
233
+ meta: string|list|boolean, optional "description"
234
+ Only relevant if `pretty=True`: controls what metadata of the feature
235
+ should be printed.
236
+
237
+ If it is None, False, or the empty string or empty list:
238
+ no metadata will be printed.
239
+
240
+ It it is the boolean value True: all metadata will be printed.
241
+
242
+ If it is a list of key names or a string with key names
243
+ separated by white-space and / or commas, only these metadata keys
244
+ will be printed.
245
+
246
+ Returns
247
+ -------
248
+ dict of dict
249
+ The features are keys, the value per feature is None or a dict with the
250
+ following information:
251
+
252
+ `None` if the feature is not loaded.
253
+
254
+ If the feature is loaded:
255
+
256
+ * `kind`: `node`, `edge`, `config`, `computed`;
257
+ * `type` is the type of values: `int`, or `str` or `""`;
258
+ * `edgeValues`: if an edge feature it indicates whether
259
+ the edges have values. Otherwise `None`.
260
+ * `meta`: dictionary containing the metadata of the feature
261
+
262
+ If `pretty`, nothing is returned, but the dict is pretty printed.
263
+ """
264
+
265
+ fNames = list(self.TF.features) if features is None else fitemize(features)
266
+ info = {}
267
+
268
+ for fName in fNames:
269
+ fMeta = {}
270
+ fType = None
271
+ edgeValues = None
272
+ fSource = None
273
+ hasInfo = True
274
+ if fName in self.TF.features:
275
+ fObj = self.TF.features[fName]
276
+ fSource = ux(fObj.dirName)
277
+ fMeta = fObj.metaData
278
+ fType = fMeta.get("valueType", "")
279
+ fMeta = {k: v for (k, v) in fMeta.items() if k != "valueType"}
280
+
281
+ isLoadedF = hasattr(self.F, fName)
282
+ isLoadedE = hasattr(self.E, fName)
283
+ if isLoadedF or isLoadedE:
284
+ if isLoadedF:
285
+ fKind = "node"
286
+ elif isLoadedE:
287
+ fKind = "edge"
288
+ flObj = getattr(self.E, fName)
289
+ edgeValues = False if fName == "oslots" else flObj.doValues
290
+ elif (
291
+ fName.startswith("__")
292
+ and fName.endswith("__")
293
+ and hasattr(self.C, fName.strip("_"))
294
+ ):
295
+ fKind = "computed"
296
+ elif fName in self.TF.features:
297
+ if fObj.isConfig:
298
+ fKind = "config"
299
+ else:
300
+ hasInfo = False
301
+ else:
302
+ hasInfo = False
303
+
304
+ info[fName] = (
305
+ dict(
306
+ kind=fKind,
307
+ type=fType,
308
+ meta=fMeta,
309
+ source=fSource,
310
+ edgeValues=edgeValues,
311
+ )
312
+ if hasInfo
313
+ else None
314
+ )
315
+ if pretty:
316
+ for fName, fInfo in sorted(info.items()):
317
+ if fInfo is None:
318
+ kind = "NOT LOADED"
319
+ kind = f" {kind:<10}"
320
+ fSource = ""
321
+ metaRep = ""
322
+ heading = f"{fName:<20}{kind}{fSource}"
323
+ else:
324
+ fKind = fInfo["kind"]
325
+ fMeta = fInfo.get("meta", {})
326
+ fType = fInfo.get("type", "")
327
+ fSource = fInfo.get("source", "") if path else ""
328
+ fSource = f" {fSource}" if fSource else ""
329
+ fEV = fInfo.get("edgeValues", "")
330
+ if valueType:
331
+ kind = (
332
+ f"node ({fType})"
333
+ if fKind == "node"
334
+ else f"edge ({fType})"
335
+ if fKind == "edge" and fEV
336
+ else "edge"
337
+ if fKind == "edge"
338
+ else f"{fKind}"
339
+ )
340
+ kind = f" {kind:<10}" if kind else ""
341
+ else:
342
+ kind = ""
343
+ if meta is True:
344
+ metaKeys = sorted(fMeta.keys())
345
+ metaInfo = fMeta
346
+ elif not meta:
347
+ metaInfo = {}
348
+ else:
349
+ metaKeys = fitemize(meta)
350
+ metaInfo = {k: fMeta[k] for k in metaKeys if k in fMeta}
351
+
352
+ heading = f"{fName:<20}{kind}{fSource}"
353
+ metaRep = ""
354
+ indent = " " * (len(heading) + 1)
355
+ if metaInfo:
356
+ if len(metaKeys) == 1:
357
+ value = metaInfo.get(metaKeys[0], "")
358
+ value = "\n".join(
359
+ wrap(value, width=80, subsequent_indent=indent)
360
+ )
361
+ metaRep = f" {value}" if value else ""
362
+ else:
363
+ indent = " " * 21
364
+ for k in metaKeys:
365
+ value = metaInfo.get(k, "")
366
+ value = "\n".join(
367
+ wrap(
368
+ value,
369
+ width=80,
370
+ subsequent_indent=f"\t{indent} ",
371
+ )
372
+ )
373
+ metaRep += f"\n\t{k:<20} = {value}"
374
+
375
+ msg = f"{heading}{metaRep}"
376
+ console(msg)
377
+ return None
378
+ return info
379
+
380
+ def makeAvailableIn(self, scope):
381
+ """Exports members of the API to the global namespace.
382
+
383
+ Only the members whose names start with a capital are exported.
384
+
385
+ If you are working with a single data source in your program, it is a bit
386
+ tedious to write the initial `TF.api.` or `A.api` all the time.
387
+ By this method you can avoid that.
388
+
389
+ !!! explanation "Longer names"
390
+ There are also longer names which can be used as aliases
391
+ to the single capital letters.
392
+ This might or might not improve the readability of your program.
393
+
394
+ short name | long name
395
+ --- | ---
396
+ `N` | `Nodes`
397
+ `F` | `Feature`
398
+ `Fs` | `FeatureString`
399
+ `Fall` | `AllFeatures`
400
+ `E` | `Edge`
401
+ `Es` | `EdgeString`
402
+ `Eall` `AllEdges`
403
+ `C` | `Computed`
404
+ `Cs` `ComputedString`
405
+ `Call` | `AllComputeds`
406
+ `L` | `Locality`
407
+ `T` | `Text`
408
+ `S` | `Search`
409
+
410
+ Parameters
411
+ ----------
412
+ scope: dict
413
+ A dictionary into which the members of the core API will be inserted.
414
+ The only sensible choice is: `globals()`.
415
+
416
+ Returns
417
+ -------
418
+ tuple
419
+ A grouped list of API members that has been hoisted to the global
420
+ scope.
421
+
422
+ Notes
423
+ -----
424
+ !!! explanation "Why pass `globals()`?"
425
+ Although we know it should always be `globals()`, we cannot
426
+ define a function that looks into the `globals()` of its caller.
427
+ So we have to pass it on.
428
+ """
429
+
430
+ for member in dir(self):
431
+ if "_" not in member and member[0].isupper():
432
+ scope[member] = getattr(self, member)
433
+ if member not in API_REFS:
434
+ console(f'WARNING: API member "{member}" not documented')
435
+
436
+ grouped = {}
437
+ for member, (head, sub, ref) in API_REFS.items():
438
+ grouped.setdefault(ref, {}).setdefault((head, sub), []).append(member)
439
+
440
+ # grouped
441
+ # node-features=>(Features, node)=>[F, ...]
442
+
443
+ docs = []
444
+ for ref, groups in sorted(grouped.items()):
445
+ chunks = []
446
+ for (head, sub), members in sorted(groups.items()):
447
+ chunks.append(" ".join(sorted(members, key=lambda x: (len(x), x))))
448
+ docs.append((head, ref, tuple(chunks)))
449
+ return docs
450
+
451
+ # docs
452
+ # (Features, node-features, ('F ...', ...))
453
+
454
+ def ensureLoaded(self, features):
455
+ """Checks if features are loaded and if not loads them.
456
+
457
+ All features in question will be made available to the core API.
458
+
459
+ Parameters
460
+ ----------
461
+ features: string | iterable of strings
462
+ It is a string containing space separated feature names,
463
+ or an iterable of feature names.
464
+ The feature names are just the names of `.tf` files
465
+ without directory information and without extension.
466
+
467
+ Returns
468
+ -------
469
+ set
470
+ The names of the features in question as a set of strings.
471
+ """
472
+
473
+ F = self.F
474
+ E = self.E
475
+ TF = self.TF
476
+ warning = TF.warning
477
+
478
+ needToLoad = set()
479
+ loadedFeatures = set()
480
+
481
+ for fName in sorted(flattenToSet(features)):
482
+ fObj = TF.features.get(fName, None)
483
+ if not fObj:
484
+ warning(f'Cannot load feature "{fName}": not in dataset')
485
+ continue
486
+ if fObj.dataLoaded and (hasattr(F, fName) or hasattr(E, fName)):
487
+ loadedFeatures.add(fName)
488
+ else:
489
+ needToLoad.add(fName)
490
+ if len(needToLoad):
491
+ TF.load(
492
+ needToLoad,
493
+ add=True,
494
+ silent=DEEP,
495
+ )
496
+ loadedFeatures |= needToLoad
497
+ return loadedFeatures
498
+
499
+ def footprint(self, recompute=False, bySize=True):
500
+ """Computes the memory footprint in RAM of the loaded TF data.
501
+
502
+ This includes the pre-computed data.
503
+
504
+ Parameters
505
+ ----------
506
+ recompute: boolean, optional False
507
+ The function looks first for earlier computed size data.
508
+ If that is found, it will be used, and no size computation will take place.
509
+ Unless this parameter is True.
510
+ If no earlier computed size data is found, sizes will be computed anyway.
511
+ bySize: boolean, optional True
512
+ Whether to sort the features by the size they occupy in RAM.
513
+ If False, the features will be sorted alphabetically.
514
+ """
515
+ if hasattr(self, "sizes") and not recompute:
516
+ sizes = self.sizes
517
+ else:
518
+ TF = self.TF
519
+ features = TF.features
520
+ nFeatures = len(features)
521
+ sizes = {}
522
+
523
+ for ft in sorted(features):
524
+ console(f"\rcomputing size of {ft:<30}", newline=False)
525
+ data = features[ft].data
526
+ if data is None:
527
+ continue
528
+ nData = len(data)
529
+ sData = deepSize(data)
530
+ sizes[ft] = (nData, sData)
531
+
532
+ console(f"\r{'':>40}", newline=False)
533
+ self.sizes = sizes
534
+
535
+ material = ""
536
+
537
+ nFeatures = len(sizes)
538
+ totals = collections.Counter()
539
+
540
+ for ft, (nData, sData) in sorted(
541
+ sizes.items(),
542
+ key=(lambda x: (-x[1][1], x[0])) if bySize else lambda x: x[0],
543
+ ):
544
+ material += f"{ft} | {nData:,} | {sData:,}\n"
545
+ totals["nData"] += nData
546
+ totals["sData"] += sData
547
+
548
+ material += f"TOTAL | {totals['nData']:,} | {totals['sData']:,}"
549
+ header = dedent(
550
+ f"""
551
+ # {nFeatures} features
552
+
553
+ feature | members | size in bytes
554
+ --- | --- | ---
555
+ """
556
+ )
557
+
558
+ print(header + material)
559
+
560
+
561
+ def addOtype(api):
562
+ setattr(api.F.otype, "all", tuple(o[0] for o in api.C.levels.data))
563
+ setattr(
564
+ api.F.otype, "support", dict(((o[0], (o[2], o[3])) for o in api.C.levels.data))
565
+ )
566
+
567
+
568
+ def addLocality(api):
569
+ api.L = Locality(api)
570
+ api.Locality = api.L
571
+
572
+
573
+ def addNodes(api):
574
+ api.N = Nodes(api)
575
+ api.Nodes = api.N
576
+
577
+
578
+ def addText(api):
579
+ api.T = Text(api)
580
+ api.Text = api.T
581
+
582
+
583
+ def addSearch(api, silent=SILENT_D):
584
+ silent = silentConvert(silent)
585
+ api.S = Search(api, silent)
586
+ api.Search = api.S