moat-db-thing 0.0.1__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,14 @@
1
+ The code in this repository, and all MoaT submodules it refers to,
2
+ is part of the MoaT project.
3
+
4
+ Unless a submodule's LICENSE.txt states otherwise, all included files are
5
+ licensed under the LGPL V3, as published by the FSF at
6
+ https://www.gnu.org/licenses/lgpl-3.0.html .
7
+
8
+ In addition to the LGPL's terms, the author(s) respectfully ask all users of
9
+ this code to contribute any bug fixes or enhancements. Also, please link back to
10
+ https://M-o-a-T.org.
11
+
12
+ Thank you.
13
+
14
+ Copyright © 2021 ff.: the MoaT contributor(s), as per the git changelog(s).
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: moat-db-thing
3
+ Version: 0.0.1
4
+ Summary: MoaT support for storing info about things
5
+ Author-email: Matthias Urlichs <matthias@urlichs.de>
6
+ Project-URL: homepage, https://m-o-a-t.org
7
+ Project-URL: repository, https://github.com/M-o-a-T/moat
8
+ Keywords: MoaT
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Framework :: Trio
13
+ Classifier: Framework :: AnyIO
14
+ Classifier: Development Status :: 4 - Beta
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/x-rst
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: anyio~=4.2
19
+ Requires-Dist: asyncclick
20
+ Requires-Dist: attrs
21
+ Requires-Dist: msgpack
22
+ Requires-Dist: moat-util~=0.60.9
23
+ Requires-Dist: moat-db~=0.2.3
24
+ Requires-Dist: moat-label~=0.2.2
25
+ Requires-Dist: moat-box~=0.2.2
26
+ Dynamic: license-file
@@ -0,0 +1,9 @@
1
+ # MoaT-Thing
2
+
3
+ % start synopsis
4
+
5
+ This module manages things. As in, the stuff you own.
6
+
7
+ This is woefully incomplete, more a concept than real code, and a work in progress.
8
+
9
+ % end synopsis
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [ "setuptools", "wheel",]
4
+
5
+ [project]
6
+ classifiers = [
7
+ "Intended Audience :: Developers",
8
+ "Programming Language :: Python :: 3",
9
+ "Framework :: AsyncIO",
10
+ "Framework :: Trio",
11
+ "Framework :: AnyIO",
12
+ "Development Status :: 4 - Beta",
13
+ ]
14
+ dependencies = [
15
+ "anyio ~= 4.2",
16
+ "asyncclick",
17
+ "attrs",
18
+ "msgpack",
19
+ "moat-util ~= 0.60.9",
20
+ "moat-db ~= 0.2.3",
21
+ "moat-label ~= 0.2.2",
22
+ "moat-box ~= 0.2.2",
23
+ ]
24
+ version = "0.0.1"
25
+ keywords = [ "MoaT",]
26
+ requires-python = ">=3.8"
27
+ name = "moat-db-thing"
28
+ description = "MoaT support for storing info about things"
29
+ readme = "README.rst"
30
+
31
+ [[project.authors]]
32
+ name = "Matthias Urlichs"
33
+ email = "matthias@urlichs.de"
34
+
35
+ [project.urls]
36
+ homepage = "https://m-o-a-t.org"
37
+ repository = "https://github.com/M-o-a-T/moat"
38
+
39
+ [tool.setuptools]
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.setuptools.package-data]
44
+ "*" = ["*.yaml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """
2
+ This subpackage manages things (stored in a database).
3
+ """
@@ -0,0 +1,232 @@
1
+ """
2
+ Support for things.
3
+ """
4
+
5
+ # The main code must not load any sqlalchemy code.
6
+ # sqlalchemy might not be present.
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+
12
+ import asyncclick as click
13
+ from sqlalchemy import select
14
+
15
+ from moat.util import load_subgroup, option_ng, yprint
16
+ from moat.db import database
17
+
18
+ from .model import Thing, ThingTyp
19
+
20
+
21
+ @load_subgroup(prefix="moat.thing", invoke_without_command=False)
22
+ @click.pass_context
23
+ @click.option("--name", "-n", type=str, help="Name of the thing type")
24
+ def cli(ctx, name):
25
+ """Things."""
26
+ obj = ctx.obj
27
+
28
+ sess = ctx.with_resource(database(obj.cfg.db))
29
+ ctx.with_resource(sess.begin())
30
+
31
+ obj.session = sess
32
+ obj.name = name
33
+
34
+
35
+ @cli.group
36
+ @click.option("--name", "-n", type=str, help="Text on the label")
37
+ @click.pass_obj
38
+ def one(obj, name):
39
+ """
40
+ Manage actual things.
41
+ """
42
+ obj.name = name
43
+ pass
44
+
45
+
46
+ @one.command(name="show")
47
+ @click.option("--type", "-t", "type_", type=str, help="Type of thing to list")
48
+ @click.pass_obj
49
+ def show_(obj, type_):
50
+ """
51
+ Show thing details / list all things of a type.
52
+
53
+ If a name is set, show details for this thing.
54
+ Otherwise list all things without storage.
55
+ """
56
+ sess = obj.session
57
+
58
+ if obj.name is not None:
59
+ thing = sess.one(Thing, name=obj.name)
60
+ yprint(thing.dump())
61
+ return
62
+
63
+ sel = select(Thing)
64
+ if type_ is None:
65
+ sel = sel.where(Thing.container == None) # noqa:E711
66
+ else:
67
+ ttyp = obj.session.one(ThingTyp, name=type_)
68
+ sel = sel.where(Thing.thingtyp == ttyp)
69
+ with sess.execute(select(Thing).where(Thing.container == None)) as things: # noqa:E711
70
+ for (thing,) in things:
71
+ print(thing.name, thing.descr)
72
+
73
+
74
+ def opts(c):
75
+ c = option_ng("--name", "-n", type=str, help="Rename this thing")(c)
76
+ c = option_ng("--x", "-x", "pos_x", type=int, help="X position in parent")(c)
77
+ c = option_ng("--y", "-y", "pos_y", type=int, help="Y position in parent")(c)
78
+ c = option_ng("--z", "-z", "pos_z", type=int, help="Z position in parent")(c)
79
+ c = option_ng("--in", "-i", "container", type=str, help="Where is it?")(c)
80
+ c = option_ng("--typ", "-t", "thingtyp", type=str, help="Type of the thing")(c)
81
+ c = option_ng("--descr", "-d", "descr", type=str, help="Description of the thing")(c)
82
+ c = option_ng("--comment", "-c", type=str, help="Additional comments")(c)
83
+ c = option_ng("--label", "-l", type=str, help="Label for this thing")(c)
84
+ return c
85
+
86
+
87
+ @one.command()
88
+ @opts
89
+ @click.pass_obj
90
+ def add(obj, **kw):
91
+ """
92
+ Add a thing.
93
+ """
94
+ if obj.name is None:
95
+ raise click.UsageError("The thing needs a name!")
96
+
97
+ try:
98
+ thing = obj.session.one(Thing, name=obj.name)
99
+ except KeyError:
100
+ pass
101
+ else:
102
+ print("This thing already exists", file=sys.stderr)
103
+ sys.exit(1)
104
+ thing = Thing(name=obj.name)
105
+ obj.session.add(thing)
106
+ thing.apply(**kw)
107
+
108
+
109
+ @one.command(epilog="Use '-x/-y/-z 0' to clear a position, '--in -' to remove the location.")
110
+ @opts
111
+ @click.pass_obj
112
+ def set(obj, **kw): # noqa: A001
113
+ """
114
+ Modify a thing.
115
+ """
116
+ if obj.name is None:
117
+ raise click.UsageError("Which thing? Use a name")
118
+
119
+ try:
120
+ thing = obj.session.one(Thing, name=obj.name)
121
+ except KeyError:
122
+ print("This thing doesn't exist", file=sys.stderr)
123
+ sys.exit(1)
124
+ thing.apply(**kw)
125
+
126
+
127
+ @one.command()
128
+ @click.pass_obj
129
+ def delete(obj):
130
+ """
131
+ Permanently remove a thing.
132
+ """
133
+ if obj.name is None:
134
+ raise click.UsageError("Which thing? Use a name")
135
+ try:
136
+ thing = obj.session.one(Thing, name=obj.name)
137
+ except KeyError:
138
+ print("This thing doesn't exist", file=sys.stderr)
139
+ sys.exit(1)
140
+ obj.session.delete(thing)
141
+
142
+
143
+ @cli.group(name="typ")
144
+ @click.option("--name", "-n", type=str, help="Name of the type")
145
+ @click.pass_context
146
+ def typ_(ctx, name):
147
+ """\
148
+ Manage a hierarchy of types of things.
149
+ """
150
+ obj = ctx.obj
151
+ if obj.name is not None:
152
+ raise click.UsageError("Please use 'thing typ -n NAME'")
153
+ obj.name = name
154
+
155
+
156
+ @typ_.command(name="show")
157
+ @click.pass_obj
158
+ def typ_show(obj):
159
+ """
160
+ Show thing details / list all thing types.
161
+
162
+ If a name is set, show details for this type.
163
+ Otherwise list all known thing types.
164
+ """
165
+ sess = obj.session
166
+
167
+ if obj.name is None:
168
+ seen = False
169
+ with sess.execute(select(ThingTyp)) as things:
170
+ for (thing,) in things:
171
+ seen = True
172
+ print(thing.name)
173
+ if not seen:
174
+ print("No thing types defined yet. Use '--help'?", file=sys.stderr)
175
+ else:
176
+ thing = sess.one(ThingTyp, name=obj.name)
177
+ yprint(thing.dump())
178
+
179
+
180
+ def typopts(c):
181
+ c = option_ng("--name", "-n", type=str, help="Rename this type")(c)
182
+ c = option_ng("--parent", "-p", type=str, help="Parent of this type")(c)
183
+ c = click.option(
184
+ "--abstract",
185
+ "-a",
186
+ is_flag=True,
187
+ help="This type can't contain a real thing",
188
+ )(c)
189
+ c = click.option("--real", "-A", is_flag=True, help="This type can have a real thing")(c)
190
+ c = option_ng("--comment", "-c", "comment", type=str, help="Description of this type")(c)
191
+ return c
192
+
193
+
194
+ @typ_.command(name="add")
195
+ @typopts
196
+ @click.pass_obj
197
+ def typ_add(obj, **kw):
198
+ """
199
+ Add a thing type.
200
+ """
201
+ if obj.name is None:
202
+ raise click.UsageError("The thing type needs a name!")
203
+
204
+ bt = ThingTyp(name=obj.name)
205
+ obj.session.add(bt)
206
+ bt.apply(**kw)
207
+
208
+
209
+ @typ_.command(name="set", epilog="Use '-i -NAME' to remove a containing thing type.")
210
+ @typopts
211
+ @click.pass_obj
212
+ def typ_set(obj, **kw):
213
+ """
214
+ Modify a thing type.
215
+ """
216
+ if obj.name is None:
217
+ raise click.UsageError("Which thing type? Use a name")
218
+
219
+ bt = obj.session.one(ThingTyp, name=obj.name)
220
+ bt.apply(**kw)
221
+
222
+
223
+ @typ_.command(name="delete")
224
+ @click.pass_obj
225
+ def typ_delete(obj):
226
+ """
227
+ Remove a thing type.
228
+ """
229
+ if obj.name is None:
230
+ raise click.UsageError("Which thing type? Use a name")
231
+ bt = obj.session.one(ThingTyp, name=obj.name)
232
+ obj.session.delete(bt)
@@ -0,0 +1,111 @@
1
+ """
2
+ Database schema for collecting things
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from sqlalchemy import ForeignKey, String, event
8
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
9
+
10
+ from moat.db.schema import Base
11
+
12
+
13
+ class ThingTyp(Base):
14
+ "One kind of thing."
15
+
16
+ id: Mapped[int] = mapped_column(primary_key=True)
17
+ name: Mapped[str] = mapped_column(unique=True, type_=String(40))
18
+ comment: Mapped[str] = mapped_column(type_=String(200), nullable=True)
19
+
20
+ parent_id: Mapped[int] = mapped_column(
21
+ ForeignKey("thingtyp.id", name="fk_thingtyp_typ"),
22
+ nullable=True,
23
+ )
24
+ parent: Mapped[ThingTyp] = relationship(
25
+ "ThingTyp",
26
+ back_populates="children",
27
+ remote_side=[id],
28
+ )
29
+
30
+ abstract: Mapped[bool] = mapped_column(
31
+ nullable=False,
32
+ default=False,
33
+ server_default="0",
34
+ comment="Must be False for instantiating things with this type",
35
+ )
36
+ children: Mapped[set[ThingTyp]] = relationship("ThingTyp", back_populates="parent")
37
+ things: Mapped[set[Thing]] = relationship(back_populates="thingtyp")
38
+
39
+ def dump(self): # noqa: D102
40
+ res = super().dump()
41
+ if self.parent:
42
+ res["parent"] = self.parent.name
43
+ if self.children:
44
+ res["child"] = [p.name for p in self.children]
45
+ if self.things:
46
+ res["things"] = len(self.things)
47
+ return res
48
+
49
+
50
+ class Thing(Base):
51
+ "One particular thing"
52
+
53
+ id: Mapped[int] = mapped_column(primary_key=True)
54
+
55
+ typ_id: Mapped[int] = mapped_column(ForeignKey("thingtyp.id", name="fk_thing_typ"))
56
+ name: Mapped[str] = mapped_column(unique=True, type_=String(40))
57
+ descr: Mapped[str] = mapped_column(type_=String(200), nullable=True)
58
+ comment: Mapped[str] = mapped_column(type_=String(200), nullable=True)
59
+
60
+ box_id: Mapped[int] = mapped_column(ForeignKey("box.id", name="fk_thing_box"), nullable=True)
61
+
62
+ thingtyp: Mapped[ThingTyp] = relationship(back_populates="things")
63
+
64
+ # possibly the location within its parent
65
+ pos_x: Mapped[int] = mapped_column(nullable=True, comment="X position in parent")
66
+ pos_y: Mapped[int] = mapped_column(nullable=True, comment="Y position in parent")
67
+ pos_z: Mapped[int] = mapped_column(nullable=True, comment="Z position in parent")
68
+
69
+ def dump(self): # noqa: D102
70
+ res = super().dump()
71
+ res.pop("pos_x", None)
72
+ res.pop("pos_y", None)
73
+ res.pop("pos_z", None)
74
+ if self.pos_x or self.pos_y or self.pos_z:
75
+ res["pos"] = [self.pos_x, self.pos_y, self.pos_z]
76
+ if self.container is not None:
77
+ res["in"] = self.container.name
78
+ if self.labels:
79
+ res["labels"] = [f"{lab.id}:{lab.text}" for lab in self.labels]
80
+ if self.thingtyp:
81
+ res["typ"] = self.thingtyp.name
82
+ return res
83
+
84
+
85
+ @event.listens_for(Thing, "before_insert")
86
+ @event.listens_for(Thing, "before_update")
87
+ def validate_thing_coords(mapper, connection, model): # noqa: D103
88
+ mapper, connection # noqa:B018
89
+ par = model.container
90
+ if par is not None:
91
+ par = par.boxtyp
92
+
93
+ def chk(p):
94
+ pp = f"pos_{p}"
95
+ ppos = getattr(par, pp, None)
96
+
97
+ if (pos := getattr(model, pp)) is None:
98
+ if ppos is not None and ppos > 1:
99
+ raise ValueError(f"Thing {model.name} needs a value for {p}")
100
+ elif pos <= 0:
101
+ raise ValueError(f"Thing {model.name} can't set {p} <= 0")
102
+ elif par is None:
103
+ raise ValueError(f"Thing {model.name} is not in a sized container")
104
+ elif ppos is None:
105
+ raise ValueError(f"Thing {model.name} can't have a value for {p}")
106
+ elif ppos < pos:
107
+ raise ValueError(f"Thing {model.name}: {p} is {pos}, max is {ppos}")
108
+
109
+ chk("x")
110
+ chk("y")
111
+ chk("z")
@@ -0,0 +1,94 @@
1
+ """
2
+ Database schema for collecting things
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from sqlalchemy import func, select
8
+ from sqlalchemy.orm import relationship
9
+
10
+ from moat.util import NotGiven
11
+ from moat.box.model import Box
12
+ from moat.db.schema import Base
13
+ from moat.db.util import session
14
+ from moat.label.model import Label
15
+
16
+ from .model import Thing, ThingTyp
17
+
18
+ Thing.labels = relationship(Label, back_populates="thing", collection_class=set)
19
+ Thing.container = relationship(Box, back_populates="things")
20
+
21
+
22
+ def thing_apply(self, label=NotGiven, container=NotGiven, thingtyp=NotGiven, **kw): # noqa: D103
23
+ sess = session.get()
24
+ with sess.no_autoflush:
25
+ Base.apply(self, **kw)
26
+
27
+ if container is NotGiven:
28
+ pass
29
+ elif container is None or container == "-":
30
+ self.container = None
31
+ else:
32
+ box = sess.one(Box, name=container)
33
+ if not box.boxtype.usable:
34
+ raise ValueError("You can't put anything into a {box.boxtype.name !r}.")
35
+ self.container = box
36
+
37
+ if label is NotGiven:
38
+ pass
39
+ elif label is None or label == "-":
40
+ self.label = None
41
+ else:
42
+ self.label = sess.one(Label, name=label)
43
+
44
+ if thingtyp is NotGiven:
45
+ if self.thingtyp is None:
46
+ raise ValueError("New things need a type")
47
+ elif thingtyp is None:
48
+ raise ValueError("Things need a type")
49
+ else:
50
+ if self.thingtyp is None:
51
+ ttyp = sess.one(ThingTyp, name=thingtyp)
52
+ if ttyp.abstract:
53
+ raise ValueError("Things need a non-abstract type")
54
+ self.thingtyp = ttyp
55
+ elif self.thingtyp.name != thingtyp:
56
+ raise ValueError("Thing types cannot be changed")
57
+
58
+
59
+ Thing.apply = thing_apply
60
+
61
+
62
+ def thingtyp_apply(self, parent=NotGiven, abstract=False, real=False, **kw): # noqa: D103
63
+ if abstract:
64
+ if real:
65
+ raise ValueError("A type can't be both 'abstract' and 'real'.")
66
+ kw["abstract"] = True
67
+ elif real:
68
+ kw["abstract"] = False
69
+
70
+ Base.apply(self, **kw)
71
+
72
+ sess = session.get()
73
+
74
+ if parent is NotGiven:
75
+ parent = self.parent.name if self.parent is not None else None
76
+ if parent is None:
77
+ (n,) = sess.execute(
78
+ select(func.count(ThingTyp.id))
79
+ .where(ThingTyp.parent == None) # noqa:E711
80
+ .where(ThingTyp.name != self.name),
81
+ ).first()
82
+ if n:
83
+ raise ValueError("Only one top entry allowed")
84
+ else:
85
+ par = sess.one(ThingTyp, name=parent)
86
+ p = par
87
+ while p is not None:
88
+ if p == self:
89
+ raise ValueError("No cycles allowed.")
90
+ p = p.parent
91
+ self.parent = par
92
+
93
+
94
+ ThingTyp.apply = thingtyp_apply
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: moat-db-thing
3
+ Version: 0.0.1
4
+ Summary: MoaT support for storing info about things
5
+ Author-email: Matthias Urlichs <matthias@urlichs.de>
6
+ Project-URL: homepage, https://m-o-a-t.org
7
+ Project-URL: repository, https://github.com/M-o-a-T/moat
8
+ Keywords: MoaT
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Framework :: Trio
13
+ Classifier: Framework :: AnyIO
14
+ Classifier: Development Status :: 4 - Beta
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/x-rst
17
+ License-File: LICENSE.txt
18
+ Requires-Dist: anyio~=4.2
19
+ Requires-Dist: asyncclick
20
+ Requires-Dist: attrs
21
+ Requires-Dist: msgpack
22
+ Requires-Dist: moat-util~=0.60.9
23
+ Requires-Dist: moat-db~=0.2.3
24
+ Requires-Dist: moat-label~=0.2.2
25
+ Requires-Dist: moat-box~=0.2.2
26
+ Dynamic: license-file
@@ -0,0 +1,12 @@
1
+ LICENSE.txt
2
+ README.md
3
+ pyproject.toml
4
+ src/moat/db/thing/__init__.py
5
+ src/moat/db/thing/_main.py
6
+ src/moat/db/thing/model.py
7
+ src/moat/db/thing/model_.py
8
+ src/moat_db_thing.egg-info/PKG-INFO
9
+ src/moat_db_thing.egg-info/SOURCES.txt
10
+ src/moat_db_thing.egg-info/dependency_links.txt
11
+ src/moat_db_thing.egg-info/requires.txt
12
+ src/moat_db_thing.egg-info/top_level.txt
@@ -0,0 +1,8 @@
1
+ anyio~=4.2
2
+ asyncclick
3
+ attrs
4
+ moat-box~=0.2.2
5
+ moat-db~=0.2.3
6
+ moat-label~=0.2.2
7
+ moat-util~=0.60.9
8
+ msgpack