From fd6006899e1816c1bad3a1a71d5297fa6f4958ab Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Mon, 11 Mar 2024 17:15:02 +0100 Subject: [PATCH 01/20] rename postgres test file; add pytest section to pyproject.toml --- .gitignore | 2 ++ pyproject.toml | 5 +++++ tests/{postgres.py => test_postgres.py} | 0 3 files changed, 7 insertions(+) rename tests/{postgres.py => test_postgres.py} (100%) diff --git a/.gitignore b/.gitignore index f625fb9..8271238 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ *.sublime-project *.sublime-workspace *.ropeproject +.env +.pytest.ini *#*# *.#* __pycache__ diff --git a/pyproject.toml b/pyproject.toml index 6f9040d..6c3e873 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,3 +27,8 @@ test = ["pytest"] [tool.setuptools] packages = ["scopes"] +[tool.pytest.ini_options] +addopts = "-vv" +python_files = "test_standard.py" # default: run only `standard` tests +# use .pytest.ini file with `python_files = test_*.py` to run all tests + diff --git a/tests/postgres.py b/tests/test_postgres.py similarity index 100% rename from tests/postgres.py rename to tests/test_postgres.py From 419902ae3f5a92511c6bbbbc8343c5e9588dd6af Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 12 Mar 2024 15:10:00 +0100 Subject: [PATCH 02/20] testing improvements; work in progress: folders traversable by zope.publisher --- demo/config.py | 6 ++++-- demo/demo_server.py | 5 +++-- scopes/server/app.py | 24 +++++++++++++++++------- scopes/storage/folder.py | 7 ++++++- tests/test_postgres.py | 14 ++++++-------- tests/test_standard.py | 13 ++++++------- tests/{tlib.py => tlib_storage.py} | 11 +++++++---- 7 files changed, 49 insertions(+), 31 deletions(-) rename tests/{tlib.py => tlib_storage.py} (88%) diff --git a/demo/config.py b/demo/config.py index 80d834c..b910662 100644 --- a/demo/config.py +++ b/demo/config.py @@ -2,17 +2,19 @@ from dotenv import load_dotenv from os import getenv -from scopes.server.app import demo_app, zope_app +from scopes.server.app import zope_app_factory load_dotenv() server_port = getenv('SERVER_PORT', '8099') -app = zope_app +app_factory = zope_app_factory # storage settings +from scopes.storage.db.postgres import StorageFactory dbengine = 'postgresql+psycopg' dbname = getenv('DBNAME', 'demo') dbuser = getenv('DBUSER', 'demo') dbpassword = getenv('DBPASSWORD', 'secret') +dbschema = getenv('DBSCHEMA', 'demo') diff --git a/demo/demo_server.py b/demo/demo_server.py index b585fac..a7f69f1 100644 --- a/demo/demo_server.py +++ b/demo/demo_server.py @@ -14,6 +14,7 @@ def run(app, config): if __name__ == '__main__': import config - run(config.app, config) - #run(config.app_factory(), config) + #run(config.app, config) + app = config.app_factory(config) + run(app, config) # see zope.app.wsgi.getWSGIApplication diff --git a/scopes/server/app.py b/scopes/server/app.py index fda9130..706bbae 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -5,6 +5,8 @@ from zope.publisher.browser import BrowserRequest from zope.publisher.publish import publish from zope.traversing.publicationtraverse import PublicationTraverser +from scopes.storage.folder import Root + def demo_app(environ, start_response): print(f'*** environ {environ}.') @@ -14,18 +16,26 @@ def demo_app(environ, start_response): return ['Hello World'.encode()] -def zope_app(environ, start_response): - request = BrowserRequest(environ['wsgi.input'], environ) - request.setPublication(DefaultPublication(AppRoot())) - request = publish(request, False) - response = request.response - start_response(response.getStatusString(), response.getHeaders()) - return response.consumeBodyIter() +def zope_app_factory(config): + config.storageFactory = config.StorageFactory(config) + def zope_app(environ, start_response): + request = BrowserRequest(environ['wsgi.input'], environ) + storage = config.storageFactory(config.dbschema) + appRoot = Root(storage, config) + request.setPublication(DefaultPublication(appRoot)) + request = publish(request, False) + response = request.response + start_response(response.getStatusString(), response.getHeaders()) + return response.consumeBodyIter() + return zope_app class AppRoot: """Zope Demo AppRoot""" + def __init__(self, config): + self.config = config + def __call__(self): """calling AppRoot""" return 'At root' diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index 9ac89be..9c3463e 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -5,6 +5,7 @@ from scopes.storage.tracking import Container, Track class Folder(Track): + """Needs docstring to be traversable.""" headFields = ['parent', 'name', 'ref'] prefix = 'fldr' @@ -30,12 +31,16 @@ class Folder(Track): value.set('name', key) self.container.save(value) + def __call__(self, request=None): + return 'folder: %s; keys: %s' % (self.name, list(self.keys())) + class Root(Folder): """A dummy (virtual) root folder for creating real folders using the Folder API.""" - def __init__(self, storage): + def __init__(self, storage, config=None): + self.config = config cont = storage.create(Folders) super(Root, self).__init__(container=cont) diff --git a/tests/test_postgres.py b/tests/test_postgres.py index a94df90..bc07c0f 100644 --- a/tests/test_postgres.py +++ b/tests/test_postgres.py @@ -3,27 +3,25 @@ """Tests for the 'scopes.storage' package - using PostgreSQL.""" import unittest +import tlib_storage +from scopes.storage.db.postgres import StorageFactory import config config.dbengine = 'postgresql+psycopg' config.dbname = 'testdb' config.dbuser = 'testuser' config.dbpassword = 'secret' +config.dbschema = 'testing' +config.storageFactory = StorageFactory(config) -# PostgreSQL-specific settings -from scopes.storage.db.postgres import StorageFactory -factory = StorageFactory(config) -storage = factory(schema='testing') - -import tlib class Test(unittest.TestCase): def test_001_tracking(self): - tlib.test_tracking(self, storage) + tlib_storage.test_tracking(self, config) def test_002_folder(self): - tlib.test_folder(self, storage) + tlib_storage.test_folder(self, config) def suite(): return unittest.TestSuite(( diff --git a/tests/test_standard.py b/tests/test_standard.py index 6ff4a7e..fb54e0a 100644 --- a/tests/test_standard.py +++ b/tests/test_standard.py @@ -3,24 +3,23 @@ """Tests for the 'scopes.storage' package.""" import unittest +import tlib_storage +from scopes.storage.common import StorageFactory import config config.dbengine = 'sqlite' config.dbname = 'var/test.db' +config.dbschema = None +config.storageFactory = StorageFactory(config) -from scopes.storage.common import StorageFactory -factory = StorageFactory(config) -storage = factory(schema=None) - -import tlib class Test(unittest.TestCase): def test_001_tracking(self): - tlib.test_tracking(self, storage) + tlib_storage.test_tracking(self, config) def test_002_folder(self): - tlib.test_folder(self, storage) + tlib_storage.test_folder(self, config) def suite(): return unittest.TestSuite(( diff --git a/tests/tlib.py b/tests/tlib_storage.py similarity index 88% rename from tests/tlib.py rename to tests/tlib_storage.py index dbb6d40..5b937d9 100644 --- a/tests/tlib.py +++ b/tests/tlib_storage.py @@ -1,10 +1,12 @@ -"""The real test implementations""" +# tests/tlib_storage.py +"""Test implementation for the `scopes.storage` package.""" from datetime import datetime from scopes.storage import folder, tracking -def test_tracking(self, storage): +def test_tracking(self, config): + storage = config.storageFactory(config.dbschema) storage.dropTable('tracks') tracks = storage.create(tracking.Container) @@ -54,9 +56,10 @@ def test_tracking(self, storage): storage.commit() -def test_folder(self, storage): +def test_folder(self, config): + storage = config.storageFactory(config.dbschema) storage.dropTable('folders') - root = folder.Root(storage) + root = folder.Root(storage, config) self.assertEqual(list(root.keys()), []) root['top'] = folder.Folder() self.assertEqual(list(root.keys()), ['top']) From 94ac7c81dbfdbc26ec15522be31830b050d2ab51 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Tue, 12 Mar 2024 22:05:28 +0100 Subject: [PATCH 03/20] work in progress: interfaces, types, views, ... --- interfaces.py | 3 +++ scopes/interfaces.py | 24 ++++++++++++++++++++++++ scopes/server/app.py | 40 +++++++--------------------------------- scopes/storage/folder.py | 6 ++++-- 4 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 interfaces.py create mode 100644 scopes/interfaces.py diff --git a/interfaces.py b/interfaces.py new file mode 100644 index 0000000..ff203cf --- /dev/null +++ b/interfaces.py @@ -0,0 +1,3 @@ +# scopes.interfaces + + diff --git a/scopes/interfaces.py b/scopes/interfaces.py new file mode 100644 index 0000000..8d1eef1 --- /dev/null +++ b/scopes/interfaces.py @@ -0,0 +1,24 @@ +# scopes.interfaces + +from zope.interface import Interface + + +class ITraversable(Interface): + + def items(): + """Return a sequence of key, value pairs of child objects.""" + + def keys(): + """Return a sequence of keys of child objects.""" + + def get(key, default): + """Return the item addressed by `key`; return `default` if not found.""" + + def __getitem__(key): + """Return the item addressed by `key`; rais KeyError if not found.""" + + def __setitem__(key, value): + """Store the `value` under the `key`. + + May modify `value` so that the attributes referencing this object + and the value object (e.g. `parent´ and `name`) are stored correctly.""" diff --git a/scopes/server/app.py b/scopes/server/app.py index 706bbae..1ed9223 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -8,47 +8,21 @@ from zope.traversing.publicationtraverse import PublicationTraverser from scopes.storage.folder import Root -def demo_app(environ, start_response): - print(f'*** environ {environ}.') - status = '200 OK' - headers = [("Content-type", "text/plain; charset=utf-8")] - start_response(status, headers) - return ['Hello World'.encode()] - - def zope_app_factory(config): - config.storageFactory = config.StorageFactory(config) + storageFactory = config.StorageFactory(config) def zope_app(environ, start_response): - request = BrowserRequest(environ['wsgi.input'], environ) - storage = config.storageFactory(config.dbschema) + storage = storageFactory(config.dbschema) appRoot = Root(storage, config) - request.setPublication(DefaultPublication(appRoot)) - request = publish(request, False) + request = BrowserRequest(environ['wsgi.input'], environ) + request.setPublication(Publication(appRoot)) + request = publish(request, True) response = request.response start_response(response.getStatusString(), response.getHeaders()) return response.consumeBodyIter() return zope_app -class AppRoot: - """Zope Demo AppRoot""" - - def __init__(self, config): - self.config = config - - def __call__(self): - """calling AppRoot""" - return 'At root' - - def __getitem__(self, key): - def child(): - """get child""" - print(f'--- getitem {key}') - return 'getitem' - return child - - def hello(self): - """method hello""" - return 'Hello AppRoot' +class Publication(DefaultPublication): + pass diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index 9c3463e..961294a 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -10,9 +10,11 @@ class Folder(Track): headFields = ['parent', 'name', 'ref'] prefix = 'fldr' + def items(self): + return ((f.name, f) for f in self.container.query(parent=self.rid)) + def keys(self): - for f in self.container.query(parent=self.rid): - yield f.name + return (k for k, v in self.items()) def get(self, key, default=None): value = self.container.queryLast(parent=self.rid, name=key) From 0bda603f1e573edbae5825d0e0380181fcef32c5 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 13 Mar 2024 09:11:28 +0100 Subject: [PATCH 04/20] first working version of URL traversal over folders via publication and view --- scopes/interfaces.py | 16 +++++++++++++--- scopes/server/app.py | 37 +++++++++++++++++++++++++++++++++++-- scopes/storage/folder.py | 6 +++++- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/scopes/interfaces.py b/scopes/interfaces.py index 8d1eef1..0fca68d 100644 --- a/scopes/interfaces.py +++ b/scopes/interfaces.py @@ -5,15 +5,18 @@ from zope.interface import Interface class ITraversable(Interface): + def get(key, default): + """Return the item addressed by `key`; return `default` if not found.""" + + +class IContainer(ITraversable): + def items(): """Return a sequence of key, value pairs of child objects.""" def keys(): """Return a sequence of keys of child objects.""" - def get(key, default): - """Return the item addressed by `key`; return `default` if not found.""" - def __getitem__(key): """Return the item addressed by `key`; rais KeyError if not found.""" @@ -22,3 +25,10 @@ class ITraversable(Interface): May modify `value` so that the attributes referencing this object and the value object (e.g. `parent´ and `name`) are stored correctly.""" + + +class IView(Interface): + + def __call__(): + """Render the view data as HTML or JSON.""" + diff --git a/scopes/server/app.py b/scopes/server/app.py index 1ed9223..b377199 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -1,10 +1,14 @@ # scopes.server.app +import json +from zope.interface import implementer from zope.publisher.base import DefaultPublication from zope.publisher.browser import BrowserRequest +from zope.publisher.interfaces import NotFound from zope.publisher.publish import publish from zope.traversing.publicationtraverse import PublicationTraverser +from scopes.interfaces import IContainer, ITraversable, IView from scopes.storage.folder import Root @@ -15,7 +19,7 @@ def zope_app_factory(config): appRoot = Root(storage, config) request = BrowserRequest(environ['wsgi.input'], environ) request.setPublication(Publication(appRoot)) - request = publish(request, True) + request = publish(request, False) response = request.response start_response(response.getStatusString(), response.getHeaders()) return response.consumeBodyIter() @@ -24,5 +28,34 @@ def zope_app_factory(config): class Publication(DefaultPublication): - pass + def traverseName(self, request, ob, name): + next = None + if ITraversable.providedBy(ob): + next = ob.get(name) + if next is None: + if name == 'index.html': + next = DefaultView(ob, request) + if next is None: + raise NotFound(ob, name, request) + return next + + def getDefaultTraversal(self, request, ob): + if IView.providedBy(ob): + return ob, () + return ob, ('index.html',) + + +@implementer(IView) +class DefaultView: + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + ob = self.context + result = dict(head=ob.head, data=ob.data) + if IContainer.providedBy(ob): + result['items'] = list(ob.keys()) + return json.dumps(result) diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index 961294a..c459ae8 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -1,9 +1,13 @@ # scopes.storage.folder +from zope.interface import implementer + +from scopes.interfaces import IContainer from scopes.storage.common import registerContainerClass from scopes.storage.tracking import Container, Track +@implementer(IContainer) class Folder(Track): """Needs docstring to be traversable.""" @@ -33,7 +37,7 @@ class Folder(Track): value.set('name', key) self.container.save(value) - def __call__(self, request=None): + def __str__(self): return 'folder: %s; keys: %s' % (self.name, list(self.keys())) From 3d10c45222427427347ff7291601773af7d4243d Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 13 Mar 2024 10:05:43 +0100 Subject: [PATCH 05/20] provide registry for views, use for default view --- scopes/server/app.py | 26 +++++------------------- scopes/server/browser.py | 43 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 scopes/server/browser.py diff --git a/scopes/server/app.py b/scopes/server/app.py index b377199..4dedf6f 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -1,14 +1,14 @@ # scopes.server.app import json -from zope.interface import implementer from zope.publisher.base import DefaultPublication from zope.publisher.browser import BrowserRequest from zope.publisher.interfaces import NotFound from zope.publisher.publish import publish from zope.traversing.publicationtraverse import PublicationTraverser -from scopes.interfaces import IContainer, ITraversable, IView +from scopes.interfaces import ITraversable, IView +from scopes.server.browser import getView from scopes.storage.folder import Root @@ -29,12 +29,11 @@ def zope_app_factory(config): class Publication(DefaultPublication): def traverseName(self, request, ob, name): - next = None + next = getView(request, ob, name) + if next is not None: + return next if ITraversable.providedBy(ob): next = ob.get(name) - if next is None: - if name == 'index.html': - next = DefaultView(ob, request) if next is None: raise NotFound(ob, name, request) return next @@ -44,18 +43,3 @@ class Publication(DefaultPublication): return ob, () return ob, ('index.html',) - -@implementer(IView) -class DefaultView: - - def __init__(self, context, request): - self.context = context - self.request = request - - def __call__(self): - ob = self.context - result = dict(head=ob.head, data=ob.data) - if IContainer.providedBy(ob): - result['items'] = list(ob.keys()) - return json.dumps(result) - diff --git a/scopes/server/browser.py b/scopes/server/browser.py new file mode 100644 index 0000000..83bc04f --- /dev/null +++ b/scopes/server/browser.py @@ -0,0 +1,43 @@ +# scopes.server.browser + +import json +from zope.interface import implementer +from scopes.interfaces import IContainer, IView + +views = {} + +def register(contextClass, name): + def doRegister(viewClass): + nameEntry = views.setdefault(name, {}) + key = contextClass and contextClass.prefix or '' + nameEntry[key] = viewClass + return viewClass + return doRegister + +def getView(request, ob, name): + nameEntry = views.get(name) + if nameEntry is None: + return None + viewClass = nameEntry.get(ob.__class__.prefix) + if viewClass is None: + viewClass = nameEntry.get('') + if viewClass is None: + return None + return viewClass(ob, request) + + +@register(None, 'index.html') +@implementer(IView) +class DefaultView: + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + ob = self.context + result = dict(head=ob.head, data=ob.data) + if IContainer.providedBy(ob): + result['items'] = list(ob.keys()) + return json.dumps(result) + From a16c1dd4695231f146dc63ff11466e652e03b2b5 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 13 Mar 2024 12:14:54 +0100 Subject: [PATCH 06/20] provide simple test for processing request using folder structure --- scopes/server/app.py | 2 -- scopes/server/browser.py | 2 ++ tests/test_standard.py | 6 +++++- tests/tlib_server.py | 26 ++++++++++++++++++++++++++ tests/tlib_storage.py | 1 + 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 tests/tlib_server.py diff --git a/scopes/server/app.py b/scopes/server/app.py index 4dedf6f..8127265 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -1,11 +1,9 @@ # scopes.server.app -import json from zope.publisher.base import DefaultPublication from zope.publisher.browser import BrowserRequest from zope.publisher.interfaces import NotFound from zope.publisher.publish import publish -from zope.traversing.publicationtraverse import PublicationTraverser from scopes.interfaces import ITraversable, IView from scopes.server.browser import getView diff --git a/scopes/server/browser.py b/scopes/server/browser.py index 83bc04f..3b44da4 100644 --- a/scopes/server/browser.py +++ b/scopes/server/browser.py @@ -27,6 +27,7 @@ def getView(request, ob, name): @register(None, 'index.html') +@register(None, 'index.json') @implementer(IView) class DefaultView: @@ -39,5 +40,6 @@ class DefaultView: result = dict(head=ob.head, data=ob.data) if IContainer.providedBy(ob): result['items'] = list(ob.keys()) + print('***', result) return json.dumps(result) diff --git a/tests/test_standard.py b/tests/test_standard.py index fb54e0a..82bd8a9 100644 --- a/tests/test_standard.py +++ b/tests/test_standard.py @@ -3,7 +3,7 @@ """Tests for the 'scopes.storage' package.""" import unittest -import tlib_storage +import tlib_server, tlib_storage from scopes.storage.common import StorageFactory import config @@ -21,6 +21,10 @@ class Test(unittest.TestCase): def test_002_folder(self): tlib_storage.test_folder(self, config) + def test_003_server(self): + tlib_server.test_app(self, config) + + def suite(): return unittest.TestSuite(( unittest.TestLoader().loadTestsFromTestCase(Test), diff --git a/tests/tlib_server.py b/tests/tlib_server.py new file mode 100644 index 0000000..aaff8c0 --- /dev/null +++ b/tests/tlib_server.py @@ -0,0 +1,26 @@ +# tests/tlib_server.py + +"""Test implementation for the `scopes.server` package.""" + +import json +from zope.publisher.browser import TestRequest +from zope.publisher.publish import publish + +from scopes.server.app import Publication +from scopes.storage.folder import Root + + +def publishRequest(config, storage, path): + appRoot = Root(storage, config) + request = TestRequest(environ=dict(PATH_INFO=path)) + request.setPublication(Publication(appRoot)) + request = publish(request, False) + return request.response + + +def test_app(self, config): + storage = config.storageFactory(config.dbschema) + response = publishRequest(config, storage, '/top') + result = json.loads(response.consumeBody()) + self.assertEqual(result['items'], ['child1']) + diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 5b937d9..57c5ff3 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -1,4 +1,5 @@ # tests/tlib_storage.py + """Test implementation for the `scopes.storage` package.""" from datetime import datetime From 7b9861600812d06714d63d14839fc8e2ad69c01d Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 13 Mar 2024 14:00:44 +0100 Subject: [PATCH 07/20] minor improvements; handle NotFound more gracefully --- scopes/server/app.py | 10 ++++++++-- scopes/server/browser.py | 1 - scopes/storage/folder.py | 3 +-- tests/tlib_server.py | 2 +- tests/tlib_storage.py | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/scopes/server/app.py b/scopes/server/app.py index 8127265..d9bc634 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -14,10 +14,10 @@ def zope_app_factory(config): storageFactory = config.StorageFactory(config) def zope_app(environ, start_response): storage = storageFactory(config.dbschema) - appRoot = Root(storage, config) + appRoot = Root(storage) request = BrowserRequest(environ['wsgi.input'], environ) request.setPublication(Publication(appRoot)) - request = publish(request, False) + request = publish(request, True) response = request.response start_response(response.getStatusString(), response.getHeaders()) return response.consumeBodyIter() @@ -41,3 +41,9 @@ class Publication(DefaultPublication): return ob, () return ob, ('index.html',) + def handleException(self, ob, request, exc_info, retry_allowed=True): + if exc_info[0] != NotFound: + raise + request.response.reset() + request.response.handleException(exc_info) + diff --git a/scopes/server/browser.py b/scopes/server/browser.py index 3b44da4..8f81074 100644 --- a/scopes/server/browser.py +++ b/scopes/server/browser.py @@ -40,6 +40,5 @@ class DefaultView: result = dict(head=ob.head, data=ob.data) if IContainer.providedBy(ob): result['items'] = list(ob.keys()) - print('***', result) return json.dumps(result) diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index c459ae8..049d21d 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -45,8 +45,7 @@ class Root(Folder): """A dummy (virtual) root folder for creating real folders using the Folder API.""" - def __init__(self, storage, config=None): - self.config = config + def __init__(self, storage): cont = storage.create(Folders) super(Root, self).__init__(container=cont) diff --git a/tests/tlib_server.py b/tests/tlib_server.py index aaff8c0..6506518 100644 --- a/tests/tlib_server.py +++ b/tests/tlib_server.py @@ -11,7 +11,7 @@ from scopes.storage.folder import Root def publishRequest(config, storage, path): - appRoot = Root(storage, config) + appRoot = Root(storage) request = TestRequest(environ=dict(PATH_INFO=path)) request.setPublication(Publication(appRoot)) request = publish(request, False) diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 57c5ff3..630d73c 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -60,7 +60,7 @@ def test_tracking(self, config): def test_folder(self, config): storage = config.storageFactory(config.dbschema) storage.dropTable('folders') - root = folder.Root(storage, config) + root = folder.Root(storage) self.assertEqual(list(root.keys()), []) root['top'] = folder.Folder() self.assertEqual(list(root.keys()), ['top']) From 0b765ad16baa37be30df8fff8eadec9eb2735801 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Wed, 13 Mar 2024 19:05:46 +0100 Subject: [PATCH 08/20] provide __repr__() and asDict() for tracks, and other improvements --- scopes/interfaces.py | 7 ++----- scopes/server/browser.py | 2 +- scopes/storage/folder.py | 10 +++++++--- scopes/storage/tracking.py | 7 +++++++ tests/tlib_server.py | 2 +- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/scopes/interfaces.py b/scopes/interfaces.py index 0fca68d..50f23f7 100644 --- a/scopes/interfaces.py +++ b/scopes/interfaces.py @@ -11,11 +11,8 @@ class ITraversable(Interface): class IContainer(ITraversable): - def items(): - """Return a sequence of key, value pairs of child objects.""" - - def keys(): - """Return a sequence of keys of child objects.""" + def values(): + """Return a sequence of child objects.""" def __getitem__(key): """Return the item addressed by `key`; rais KeyError if not found.""" diff --git a/scopes/server/browser.py b/scopes/server/browser.py index 8f81074..19b517d 100644 --- a/scopes/server/browser.py +++ b/scopes/server/browser.py @@ -39,6 +39,6 @@ class DefaultView: ob = self.context result = dict(head=ob.head, data=ob.data) if IContainer.providedBy(ob): - result['items'] = list(ob.keys()) + result['items'] = [v.asDict() for v in ob.values()] return json.dumps(result) diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index 049d21d..c0a8c25 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -14,11 +14,14 @@ class Folder(Track): headFields = ['parent', 'name', 'ref'] prefix = 'fldr' + def values(self): + return self.container.query(parent=self.rid) + def items(self): - return ((f.name, f) for f in self.container.query(parent=self.rid)) + return ((v.name, v) for v in self.values()) def keys(self): - return (k for k, v in self.items()) + return (v.name for v in self.values()) def get(self, key, default=None): value = self.container.queryLast(parent=self.rid, name=key) @@ -38,7 +41,8 @@ class Folder(Track): self.container.save(value) def __str__(self): - return 'folder: %s; keys: %s' % (self.name, list(self.keys())) + return '%s: %s; keys: %s' % (self.__class__.__name__, + self.name, list(self.keys())) class Root(Folder): diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index aa907c6..35c4213 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -62,6 +62,13 @@ class Track(object): return '' return str(self.trackId) + def __repr__(self): + return '%s: %s' % (self.__class__.__name__, self.asDict()) + + def asDict(self): + return dict(uid=self.uid, head=self.head, data=self.data, + timeStamp = str(self.timeStamp)[:19]) + @registerContainerClass class Container(object): diff --git a/tests/tlib_server.py b/tests/tlib_server.py index 6506518..949d482 100644 --- a/tests/tlib_server.py +++ b/tests/tlib_server.py @@ -22,5 +22,5 @@ def test_app(self, config): storage = config.storageFactory(config.dbschema) response = publishRequest(config, storage, '/top') result = json.loads(response.consumeBody()) - self.assertEqual(result['items'], ['child1']) + self.assertEqual(result['items'][0]['head']['name'], 'child1') From 3e1127e798168a8b15a5d1a2063c0bb2990c06b8 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 14 Mar 2024 10:21:43 +0100 Subject: [PATCH 09/20] work in progress: concept map, starting with types --- scopes/interfaces.py | 15 +++++++++-- scopes/server/browser.py | 15 ++++++----- scopes/storage/concept.py | 56 ++++++++++++++++++++++++++++++++++++++- scopes/storage/folder.py | 1 - tests/test_standard.py | 5 +++- tests/tlib_storage.py | 11 +++++++- 6 files changed, 91 insertions(+), 12 deletions(-) diff --git a/scopes/interfaces.py b/scopes/interfaces.py index 50f23f7..194e4f1 100644 --- a/scopes/interfaces.py +++ b/scopes/interfaces.py @@ -5,7 +5,7 @@ from zope.interface import Interface class ITraversable(Interface): - def get(key, default): + def get(key, default=None): """Return the item addressed by `key`; return `default` if not found.""" @@ -15,7 +15,7 @@ class IContainer(ITraversable): """Return a sequence of child objects.""" def __getitem__(key): - """Return the item addressed by `key`; rais KeyError if not found.""" + """Return the item addressed by `key`; raise KeyError if not found.""" def __setitem__(key, value): """Store the `value` under the `key`. @@ -24,6 +24,17 @@ class IContainer(ITraversable): and the value object (e.g. `parent´ and `name`) are stored correctly.""" +class IConcept(IContainer): + + def parents(*predicates): + """Return a sequence of `Triple`s in which this object is + referenced as `second`.""" + + def children(*predicates): + """Return a sequence of `Triple`s in which this object is + referenced as `first`.""" + + class IView(Interface): def __call__(): diff --git a/scopes/server/browser.py b/scopes/server/browser.py index 19b517d..4cd432a 100644 --- a/scopes/server/browser.py +++ b/scopes/server/browser.py @@ -4,13 +4,16 @@ import json from zope.interface import implementer from scopes.interfaces import IContainer, IView -views = {} +views = {} # registry for all views: {name: {prefix: viewClass, ...}, ...} -def register(contextClass, name): +def register(name, *contextClasses): + """Use as decorator: `@register(name, class, ...). + class `None` means default view for all classes.""" def doRegister(viewClass): nameEntry = views.setdefault(name, {}) - key = contextClass and contextClass.prefix or '' - nameEntry[key] = viewClass + for cl in contextClasses: + key = cl and cl.prefix or '' + nameEntry[key] = viewClass return viewClass return doRegister @@ -26,8 +29,8 @@ def getView(request, ob, name): return viewClass(ob, request) -@register(None, 'index.html') -@register(None, 'index.json') +@register('index.html', None) +@register('index.json', None) @implementer(IView) class DefaultView: diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index 95ede52..df962cb 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -1,12 +1,66 @@ # scopes.storage.concept -"""Abstract base classes for concept map application classes.""" +"""Core classes for concept map structure.""" +from zope.interface import implementer +from scopes.interfaces import IConcept from scopes.storage.common import registerContainerClass from scopes.storage.tracking import Container, Track +@implementer(IConcept) class Concept(Track): headFields = ['name'] + + +class Concepts(Container): + + insertOnChange = False + +class Predicate(Concept): + + prefix = 'pred' + + +@registerContainerClass +class Predicates(Concepts): + + itemFactory = Predicate + tableName = 'preds' + + +class Triple(Track): + + headFields = ['first', 'second', 'predicate'] + prefix = 'rel' + + +@registerContainerClass +class Rels(Container): + + itemFactory = Triple + indexes = [('first', 'predicate', 'second'), + ('first', 'second'), ('predicate', 'second')] + tableName = 'rels' + insertOnChange = False + + +# types stuff + +class Type(Concept): + + headFields = ['name', 'prefix'] + prefix = 'type' + + def get(key, default=None): + return self.container.queryLast(name=key) or default + + +@registerContainerClass +class Types(Concepts): + + itemFactory = Type + indexes = [('name',), ('prefix',)] + tableName = 'types' diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index c0a8c25..5057372 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -9,7 +9,6 @@ from scopes.storage.tracking import Container, Track @implementer(IContainer) class Folder(Track): - """Needs docstring to be traversable.""" headFields = ['parent', 'name', 'ref'] prefix = 'fldr' diff --git a/tests/test_standard.py b/tests/test_standard.py index 82bd8a9..207f3b1 100644 --- a/tests/test_standard.py +++ b/tests/test_standard.py @@ -21,7 +21,10 @@ class Test(unittest.TestCase): def test_002_folder(self): tlib_storage.test_folder(self, config) - def test_003_server(self): + def test_003_type(self): + tlib_storage.test_type(self, config) + + def test_013_server(self): tlib_server.test_app(self, config) diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 630d73c..0c6c70b 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -3,7 +3,7 @@ """Test implementation for the `scopes.storage` package.""" from datetime import datetime -from scopes.storage import folder, tracking +from scopes.storage import concept, folder, tracking def test_tracking(self, config): @@ -73,3 +73,12 @@ def test_folder(self, config): storage.commit() + +def test_type(self, config): + storage = config.storageFactory(config.dbschema) + storage.dropTable('types') + types = storage.create(concept.Types) + tid01 = types.save(concept.Type('type', 'type')) + self.assertEqual(tid01, 1) + + storage.commit() From 1e165a711c488a340b33d7870059214466ebd2af Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 14 Mar 2024 11:30:59 +0100 Subject: [PATCH 10/20] fix type methods --- scopes/storage/common.py | 10 +++++++--- scopes/storage/concept.py | 15 ++++++++++----- scopes/storage/tracking.py | 6 +++++- tests/tlib_storage.py | 12 ++++++++++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/scopes/storage/common.py b/scopes/storage/common.py index 78c2850..e0d5995 100644 --- a/scopes/storage/common.py +++ b/scopes/storage/common.py @@ -33,12 +33,16 @@ class Storage(object): def add(self, container): self.containers[container.itemFactory.prefix] = container + def getContainer(self, prefix): + container = self.containers.get(prefix) + if container is None: + return self.create(registry[prefix]) + return container + def getItem(self, uid): prefix, id = uid.split('-') id = int(id) - container = self.containers.get(prefix) - if container is None: - container = self.create(registry[prefix]) + container = self.getContainer(prefix) return container.get(id) def getExistingTable(self, tableName): diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index df962cb..129bd05 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -4,7 +4,7 @@ from zope.interface import implementer from scopes.interfaces import IConcept -from scopes.storage.common import registerContainerClass +from scopes.storage.common import registerContainerClass, registry from scopes.storage.tracking import Container, Track @@ -51,16 +51,21 @@ class Rels(Container): class Type(Concept): - headFields = ['name', 'prefix'] + headFields = ['name', 'tprefix'] prefix = 'type' - def get(key, default=None): - return self.container.queryLast(name=key) or default + def get(self, key, default=None): + cont = self.container.storage.getContainer(self.tprefix) + return cont.queryLast(name=key) or default + + def values(self): + cont = self.container.storage.getContainer(self.tprefix) + return cont.query() @registerContainerClass class Types(Concepts): itemFactory = Type - indexes = [('name',), ('prefix',)] + indexes = [('name',), ('tprefix',)] tableName = 'types' diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index 35c4213..e33e574 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -98,8 +98,11 @@ class Container(object): return tr def query(self, **crit): - stmt = self.table.select().where( + if crit: + stmt = self.table.select().where( and_(*self.setupWhere(crit))).order_by(self.table.c.trackid) + else: + stmt = self.table.select().order_by(self.table.c.trackid) for r in self.session.execute(stmt): yield self.makeTrack(r) @@ -109,6 +112,7 @@ class Container(object): return self.makeTrack(self.session.execute(stmt).first()) def save(self, track): + track.container = self crit = dict((hf, track.head[hf]) for hf in track.headFields) found = self.queryLast(**crit) if found is None: diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 0c6c70b..a0b1056 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -78,7 +78,15 @@ def test_type(self, config): storage = config.storageFactory(config.dbschema) storage.dropTable('types') types = storage.create(concept.Types) - tid01 = types.save(concept.Type('type', 'type')) - self.assertEqual(tid01, 1) + ttype = concept.Type('type', concept.Type.prefix) + ttid = types.save(ttype) + self.assertEqual(ttid, 1) + tps = list(ttype.values()) + self.assertEqual(len(tps), 1) + + tfolder = concept.Type('folder', folder.Folder.prefix) + tfid = types.save(tfolder) + fldrs = list(tfolder.values()) + self.assertEqual(len(fldrs), 2) storage.commit() From 1bf5e82afc49527789cbe4cb066a51ab254f6f0e Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Thu, 14 Mar 2024 20:16:25 +0100 Subject: [PATCH 11/20] provide (almost) automatic creation of type records in database --- scopes/storage/concept.py | 13 +++++++++++++ tests/tlib_storage.py | 16 ++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index 129bd05..daf7c94 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -17,6 +17,7 @@ class Concept(Track): class Concepts(Container): insertOnChange = False + indexes = None class Predicate(Concept): @@ -69,3 +70,15 @@ class Types(Concepts): itemFactory = Type indexes = [('name',), ('tprefix',)] tableName = 'types' + + +def storeType(storage, cls, name): + types = storage.create(Types) + types.save(Type(name, cls.prefix)) + storage.commit() + +def setupCoreTypes(storage): + for c in registry.values(): + cls = c.itemFactory + storeType(storage, cls, cls.__name__.lower()) + diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index a0b1056..1e05ba7 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -77,16 +77,16 @@ def test_folder(self, config): def test_type(self, config): storage = config.storageFactory(config.dbschema) storage.dropTable('types') - types = storage.create(concept.Types) - ttype = concept.Type('type', concept.Type.prefix) - ttid = types.save(ttype) - self.assertEqual(ttid, 1) - tps = list(ttype.values()) - self.assertEqual(len(tps), 1) + concept.setupCoreTypes(storage) - tfolder = concept.Type('folder', folder.Folder.prefix) - tfid = types.save(tfolder) + types = storage.getContainer(concept.Type.prefix) + tps = list(types.query()) + self.assertEqual(len(tps), 5) + self.assertEqual(tps[0].name, 'track') + + tfolder = types.queryLast(name='folder') fldrs = list(tfolder.values()) self.assertEqual(len(fldrs), 2) + self.assertEqual(fldrs[0].name, 'top') storage.commit() From a108ad9fc307b37fdb87f3a8e958b650f46ec135 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 15 Mar 2024 09:15:41 +0100 Subject: [PATCH 12/20] work in progress: rels --- scopes/interfaces.py | 11 ----------- scopes/storage/concept.py | 32 +++++++++++++++++++++++++++++--- scopes/storage/tracking.py | 2 +- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/scopes/interfaces.py b/scopes/interfaces.py index 194e4f1..82cad03 100644 --- a/scopes/interfaces.py +++ b/scopes/interfaces.py @@ -24,17 +24,6 @@ class IContainer(ITraversable): and the value object (e.g. `parent´ and `name`) are stored correctly.""" -class IConcept(IContainer): - - def parents(*predicates): - """Return a sequence of `Triple`s in which this object is - referenced as `second`.""" - - def children(*predicates): - """Return a sequence of `Triple`s in which this object is - referenced as `first`.""" - - class IView(Interface): def __call__(): diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index daf7c94..ba0764a 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -3,23 +3,39 @@ """Core classes for concept map structure.""" from zope.interface import implementer -from scopes.interfaces import IConcept from scopes.storage.common import registerContainerClass, registry from scopes.storage.tracking import Container, Track -@implementer(IConcept) class Concept(Track): headFields = ['name'] + def parents(self, predicate=None): + return self.container.queryRels(second=self, predicate=predicate) + + def children(self, predicate=None): + return self.container.queryRels(first=self, predicate=predicate) + class Concepts(Container): insertOnChange = False indexes = None + + def queryRels(self, **crit): + pred = crit.get(predicate) + if pred is not None and isinstance(pred, ('string', 'bytes')): + crit['predicate'] = self.storage.getContainer('pred').queryLast(name=pred) + for k, v in crit.items: + if isinstance(v, Track): + crit[k] = v.uid + rels = self.storage.getContainer('rel') + return rels.query(**crit) + # implementation of relationships between concepts using RDF-like triples + class Predicate(Concept): prefix = 'pred' @@ -37,6 +53,15 @@ class Triple(Track): headFields = ['first', 'second', 'predicate'] prefix = 'rel' + def getFirst(self): + return self.container.storage.getItem(self.first) + + def getSecond(self): + return self.container.storage.getItem(self.second) + + def getPredicate(self): + return self.container.storage.getItem(self.second) + @registerContainerClass class Rels(Container): @@ -47,6 +72,8 @@ class Rels(Container): tableName = 'rels' insertOnChange = False + defaultPredicate = 'standard' + # types stuff @@ -75,7 +102,6 @@ class Types(Concepts): def storeType(storage, cls, name): types = storage.create(Types) types.save(Type(name, cls.prefix)) - storage.commit() def setupCoreTypes(storage): for c in registry.values(): diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index e33e574..e2ebe59 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -169,7 +169,7 @@ class Container(object): *r[1:-2], trackId=r[0],timeStamp=r[-2], data=r[-1], container=self) def setupWhere(self, crit): - return [self.table.c[k.lower()] == v for k, v in crit.items()] + return [self.table.c[k.lower()] == v for k, v in crit.items() if v is not None] def setupValues(self, track, withTrackId=False): values = {} From 6cc513655073f72b7d8ff6e6fecdfa4e8d523d58 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 15 Mar 2024 11:35:07 +0100 Subject: [PATCH 13/20] view: show target with items --- scopes/interfaces.py | 9 +++++++++ scopes/server/app.py | 1 + scopes/server/browser.py | 22 ++++++++++++++++++---- scopes/storage/concept.py | 23 ++++++++++++++++++++--- scopes/storage/folder.py | 15 ++++++++++++--- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/scopes/interfaces.py b/scopes/interfaces.py index 82cad03..e146293 100644 --- a/scopes/interfaces.py +++ b/scopes/interfaces.py @@ -24,6 +24,15 @@ class IContainer(ITraversable): and the value object (e.g. `parent´ and `name`) are stored correctly.""" +class IReference(Interface): + + def getTarget(): + """Return item referenced by this object.""" + + def setTarget(target): + """Store reference to target item.""" + + class IView(Interface): def __call__(): diff --git a/scopes/server/app.py b/scopes/server/app.py index d9bc634..fb35a69 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -7,6 +7,7 @@ from zope.publisher.publish import publish from scopes.interfaces import ITraversable, IView from scopes.server.browser import getView +import scopes.storage.concept from scopes.storage.folder import Root diff --git a/scopes/server/browser.py b/scopes/server/browser.py index 4cd432a..902c8a9 100644 --- a/scopes/server/browser.py +++ b/scopes/server/browser.py @@ -2,7 +2,7 @@ import json from zope.interface import implementer -from scopes.interfaces import IContainer, IView +from scopes.interfaces import IContainer, IReference, IView views = {} # registry for all views: {name: {prefix: viewClass, ...}, ...} @@ -38,10 +38,24 @@ class DefaultView: self.context = context self.request = request - def __call__(self): + def prepareResult(self): ob = self.context - result = dict(head=ob.head, data=ob.data) + result = ob.asDict() if IContainer.providedBy(ob): result['items'] = [v.asDict() for v in ob.values()] - return json.dumps(result) + if IReference.providedBy(ob): + target = ob.getTarget() + if target: + result['target'] = target.asDict() + if IContainer.providedBy(target): + result['target']['items'] = [v.asDict() for v in target.values()] + return result + + def renderJson(self, result): + self.request.response.setHeader('Content-type', 'application/json; charset=utf-8') + return json.dumps(result).encode('UTF-8') + + def __call__(self): + result = self.prepareResult() + return self.renderJson(result) diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index ba0764a..595ede5 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -3,6 +3,7 @@ """Core classes for concept map structure.""" from zope.interface import implementer +from scopes.interfaces import IContainer from scopes.storage.common import registerContainerClass, registry from scopes.storage.tracking import Container, Track @@ -17,6 +18,10 @@ class Concept(Track): def children(self, predicate=None): return self.container.queryRels(first=self, predicate=predicate) + def values(self): + return (t.getSecond() for t in self.children(Rels.defaultPredicate)) + + class Concepts(Container): @@ -77,18 +82,30 @@ class Rels(Container): # types stuff +@implementer(IContainer) class Type(Concept): headFields = ['name', 'tprefix'] prefix = 'type' + def values(self): + cont = self.container.storage.getContainer(self.tprefix) + return cont.query() + def get(self, key, default=None): cont = self.container.storage.getContainer(self.tprefix) return cont.queryLast(name=key) or default - def values(self): + def __getitem__(self, key): + value = self.get(key) + if value is None: + raise KeyError(key) + return value + + def __setitem__(self, key, value): cont = self.container.storage.getContainer(self.tprefix) - return cont.query() + value.name = key + cont.save(value) @registerContainerClass @@ -100,7 +117,7 @@ class Types(Concepts): def storeType(storage, cls, name): - types = storage.create(Types) + types = storage.getContainer('type') types.save(Type(name, cls.prefix)) def setupCoreTypes(storage): diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index 5057372..1a05e61 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -2,12 +2,12 @@ from zope.interface import implementer -from scopes.interfaces import IContainer +from scopes.interfaces import IContainer, IReference from scopes.storage.common import registerContainerClass from scopes.storage.tracking import Container, Track -@implementer(IContainer) +@implementer(IContainer, IReference) class Folder(Track): headFields = ['parent', 'name', 'ref'] @@ -29,7 +29,7 @@ class Folder(Track): return value def __getitem__(self, key): - value = self.container.queryLast(parent=self.rid, name=key) + value = self.get(key) if value is None: raise KeyError(key) return value @@ -39,6 +39,15 @@ class Folder(Track): value.set('name', key) self.container.save(value) + def getTarget(self): + if self.ref == '': + return None + return self.container.storage.getItem(self.ref) + + def setTarget(self, target): + self.ref = target.uid + self.container.save(self) + def __str__(self): return '%s: %s; keys: %s' % (self.__class__.__name__, self.name, list(self.keys())) From 20520a70c574645f87ca89fbc07c71af62971b0e Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 15 Mar 2024 14:51:54 +0100 Subject: [PATCH 14/20] minor refactorings --- scopes/server/app.py | 2 +- scopes/server/browser.py | 34 ++++++++++++++++++---------------- scopes/storage/concept.py | 1 - 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/scopes/server/app.py b/scopes/server/app.py index fb35a69..76133a9 100644 --- a/scopes/server/app.py +++ b/scopes/server/app.py @@ -7,7 +7,7 @@ from zope.publisher.publish import publish from scopes.interfaces import ITraversable, IView from scopes.server.browser import getView -import scopes.storage.concept +import scopes.storage.concept # register container classes from scopes.storage.folder import Root diff --git a/scopes/server/browser.py b/scopes/server/browser.py index 902c8a9..1b74dfb 100644 --- a/scopes/server/browser.py +++ b/scopes/server/browser.py @@ -9,35 +9,40 @@ views = {} # registry for all views: {name: {prefix: viewClass, ...}, ...} def register(name, *contextClasses): """Use as decorator: `@register(name, class, ...). class `None` means default view for all classes.""" - def doRegister(viewClass): + def doRegister(factory): + implementer(IView)(factory) nameEntry = views.setdefault(name, {}) for cl in contextClasses: - key = cl and cl.prefix or '' - nameEntry[key] = viewClass - return viewClass + nameEntry[cl.prefix] = factory + else: + nameEntry[''] = factory + return factory return doRegister def getView(request, ob, name): nameEntry = views.get(name) if nameEntry is None: return None - viewClass = nameEntry.get(ob.__class__.prefix) - if viewClass is None: - viewClass = nameEntry.get('') - if viewClass is None: + factory = nameEntry.get(ob.prefix) + if factory is None: + factory = nameEntry.get('') + if factory is None: return None - return viewClass(ob, request) + return factory(ob, request) -@register('index.html', None) -@register('index.json', None) -@implementer(IView) +@register('index.html') +@register('index.json') class DefaultView: def __init__(self, context, request): self.context = context self.request = request + def __call__(self): + result = self.prepareResult() + return self.render(result) + def prepareResult(self): ob = self.context result = ob.asDict() @@ -51,11 +56,8 @@ class DefaultView: result['target']['items'] = [v.asDict() for v in target.values()] return result - def renderJson(self, result): + def render(self, result): self.request.response.setHeader('Content-type', 'application/json; charset=utf-8') return json.dumps(result).encode('UTF-8') - def __call__(self): - result = self.prepareResult() - return self.renderJson(result) diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index 595ede5..a968ae4 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -22,7 +22,6 @@ class Concept(Track): return (t.getSecond() for t in self.children(Rels.defaultPredicate)) - class Concepts(Container): insertOnChange = False From 606284791d320aab1dd62be4d314f0340e91d14d Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Fri, 15 Mar 2024 21:16:45 +0100 Subject: [PATCH 15/20] work in progress: topics (as example for concepts) --- scopes/storage/concept.py | 9 ++- scopes/storage/topic.py | 18 +++++ tests/test_standard.py | 3 + tests/tlib_storage.py | 143 +++++++++++++++++++++----------------- 4 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 scopes/storage/topic.py diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index a968ae4..a1ace32 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -52,6 +52,13 @@ class Predicates(Concepts): tableName = 'preds' +defaultPredicate = 'standard' + +def storePredicate(storage, name): + preds = storage.getContainer('pred') + preds.save(Predicate(name)) + + class Triple(Track): headFields = ['first', 'second', 'predicate'] @@ -76,8 +83,6 @@ class Rels(Container): tableName = 'rels' insertOnChange = False - defaultPredicate = 'standard' - # types stuff diff --git a/scopes/storage/topic.py b/scopes/storage/topic.py new file mode 100644 index 0000000..2947c4e --- /dev/null +++ b/scopes/storage/topic.py @@ -0,0 +1,18 @@ +# scopes.storage.topic + +from scopes.storage.common import registerContainerClass +from scopes.storage import concept + +class Topic(concept.Concept): + + prefix = 'tpc' + + +@registerContainerClass +class Topics(concept.Concepts): + + itemFactory = Topic + tableName = 'topics' + + + diff --git a/tests/test_standard.py b/tests/test_standard.py index 207f3b1..0dcfcbb 100644 --- a/tests/test_standard.py +++ b/tests/test_standard.py @@ -24,6 +24,9 @@ class Test(unittest.TestCase): def test_003_type(self): tlib_storage.test_type(self, config) + def test_004_topic(self): + tlib_storage.test_topic(self, config) + def test_013_server(self): tlib_server.test_app(self, config) diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 1e05ba7..3a839bc 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -3,90 +3,105 @@ """Test implementation for the `scopes.storage` package.""" from datetime import datetime -from scopes.storage import concept, folder, tracking +from scopes.storage import concept, folder, topic, tracking def test_tracking(self, config): - storage = config.storageFactory(config.dbschema) - storage.dropTable('tracks') - tracks = storage.create(tracking.Container) + storage = config.storageFactory(config.dbschema) + storage.dropTable('tracks') + tracks = storage.create(tracking.Container) - tr01 = tracking.Track('t01', 'john') - tr01.update(dict(activity='testing')) - self.assertEqual(tr01.head, {'taskId': 't01', 'userName': 'john'}) - self.assertEqual(tr01.taskId, 't01') - self.assertEqual(tr01.userName, 'john') + tr01 = tracking.Track('t01', 'john') + tr01.update(dict(activity='testing')) + self.assertEqual(tr01.head, {'taskId': 't01', 'userName': 'john'}) + self.assertEqual(tr01.taskId, 't01') + self.assertEqual(tr01.userName, 'john') - self.assertTrue(tracks.getTable() is not None) + self.assertTrue(tracks.getTable() is not None) - trid01 = tracks.save(tr01) - self.assertTrue(trid01 > 0) + trid01 = tracks.save(tr01) + self.assertTrue(trid01 > 0) - #tr01a = tracks.get(trid01) - tr01a = tracks['%07i' % trid01] - self.assertEqual(tr01a.head, tr01.head) - self.assertEqual(tr01a.trackId, trid01) - self.assertEqual(tr01a.data.get('activity'), 'testing') + #tr01a = tracks.get(trid01) + tr01a = tracks['%07i' % trid01] + self.assertEqual(tr01a.head, tr01.head) + self.assertEqual(tr01a.trackId, trid01) + self.assertEqual(tr01a.data.get('activity'), 'testing') - tr01a.update(dict(text='Set up unit tests.')) - tr01a.timeStamp = None - self.assertTrue(tracks.save(tr01a) > 0) + tr01a.update(dict(text='Set up unit tests.')) + tr01a.timeStamp = None + self.assertTrue(tracks.save(tr01a) > 0) - tr01b = tracks.queryLast(taskId='t01') - self.assertEqual(tr01b.head, tr01.head) - self.assertNotEqual(tr01b.trackId, trid01) - self.assertEqual(tr01b.data.get('activity'), 'testing') + tr01b = tracks.queryLast(taskId='t01') + self.assertEqual(tr01b.head, tr01.head) + self.assertNotEqual(tr01b.trackId, trid01) + self.assertEqual(tr01b.data.get('activity'), 'testing') - tr02 = tracking.Track('t02', 'jim', trackId=31, timeStamp=datetime(2023, 11, 30), - data=dict(activity='concept')) - trid02 = tracks.upsert(tr02) - self.assertEqual(trid02, 31) - self.assertEqual(tr02.uid, 'rec-31') - tr02.trackId = trid01 - trid021 = tracks.upsert(tr02) - self.assertEqual(trid021, trid01) - self.assertEqual(tr02.uid, 'rec-' + str(trid01)) + tr02 = tracking.Track('t02', 'jim', trackId=31, timeStamp=datetime(2023, 11, 30), + data=dict(activity='concept')) + trid02 = tracks.upsert(tr02) + self.assertEqual(trid02, 31) + self.assertEqual(tr02.uid, 'rec-31') + tr02.trackId = trid01 + trid021 = tracks.upsert(tr02) + self.assertEqual(trid021, trid01) + self.assertEqual(tr02.uid, 'rec-' + str(trid01)) - tr03 = storage.getItem('rec-31') - self.assertEqual(tr03.trackId, 31) + tr03 = storage.getItem('rec-31') + self.assertEqual(tr03.trackId, 31) - n = tracks.remove(31) - self.assertEqual(n, 1) - self.assertEqual(tracks.get(31), None) + n = tracks.remove(31) + self.assertEqual(n, 1) + self.assertEqual(tracks.get(31), None) - storage.commit() + storage.commit() def test_folder(self, config): - storage = config.storageFactory(config.dbschema) - storage.dropTable('folders') - root = folder.Root(storage) - self.assertEqual(list(root.keys()), []) - root['top'] = folder.Folder() - self.assertEqual(list(root.keys()), ['top']) - top = root['top'] - top['child1'] = folder.Folder(data=dict(title='First Child')) - self.assertEqual(list(top.keys()), ['child1']) - ch1 = top['child1'] - self.assertEqual(ch1.parent, top.rid) - self.assertEqual(list(top.keys()), ['child1']) + storage = config.storageFactory(config.dbschema) + storage.dropTable('folders') + root = folder.Root(storage) + self.assertEqual(list(root.keys()), []) + root['top'] = folder.Folder() + self.assertEqual(list(root.keys()), ['top']) + top = root['top'] + top['child1'] = folder.Folder(data=dict(title='First Child')) + self.assertEqual(list(top.keys()), ['child1']) + ch1 = top['child1'] + self.assertEqual(ch1.parent, top.rid) + self.assertEqual(list(top.keys()), ['child1']) - storage.commit() + storage.commit() def test_type(self, config): - storage = config.storageFactory(config.dbschema) - storage.dropTable('types') - concept.setupCoreTypes(storage) + storage = config.storageFactory(config.dbschema) + storage.dropTable('types') + concept.setupCoreTypes(storage) - types = storage.getContainer(concept.Type.prefix) - tps = list(types.query()) - self.assertEqual(len(tps), 5) - self.assertEqual(tps[0].name, 'track') + types = storage.getContainer(concept.Type.prefix) + tps = list(types.query()) + self.assertEqual(len(tps), 6) + self.assertEqual(tps[0].name, 'track') - tfolder = types.queryLast(name='folder') - fldrs = list(tfolder.values()) - self.assertEqual(len(fldrs), 2) - self.assertEqual(fldrs[0].name, 'top') + tfolder = types.queryLast(name='folder') + fldrs = list(tfolder.values()) + self.assertEqual(len(fldrs), 2) + self.assertEqual(fldrs[0].name, 'top') - storage.commit() + storage.commit() + + +def test_topic(self, config): + storage = config.storageFactory(config.dbschema) + storage.dropTable('topics') + topics = storage.getContainer(topic.Topic.prefix) + concept.storePredicate(storage, concept.defaultPredicate) + root = folder.Root(storage) + root['top']['topics'] = folder.Folder() + + tp_itc = topic.Topic('itc') + topics.save(tp_itc) + + storage.commit() + From f6308ff9bfcc2fafb199646c97887ac0842734b7 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 16 Mar 2024 09:42:31 +0100 Subject: [PATCH 16/20] use item class as parameter for Storage.getContainer() - never use prefix explictly in Code --- scopes/storage/common.py | 8 ++++---- scopes/storage/concept.py | 18 +++++++++++------- tests/tlib_storage.py | 4 ++-- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/scopes/storage/common.py b/scopes/storage/common.py index e0d5995..88a1818 100644 --- a/scopes/storage/common.py +++ b/scopes/storage/common.py @@ -33,7 +33,8 @@ class Storage(object): def add(self, container): self.containers[container.itemFactory.prefix] = container - def getContainer(self, prefix): + def getContainer(self, itemClass): + prefix = itemClass.prefix container = self.containers.get(prefix) if container is None: return self.create(registry[prefix]) @@ -41,9 +42,8 @@ class Storage(object): def getItem(self, uid): prefix, id = uid.split('-') - id = int(id) - container = self.getContainer(prefix) - return container.get(id) + cls = registry[prefix].itemFactory + return self.getContainer(cls).get(int(id)) def getExistingTable(self, tableName): metadata = self.metadata diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index a1ace32..26b37db 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -30,11 +30,11 @@ class Concepts(Container): def queryRels(self, **crit): pred = crit.get(predicate) if pred is not None and isinstance(pred, ('string', 'bytes')): - crit['predicate'] = self.storage.getContainer('pred').queryLast(name=pred) + crit['predicate'] = self.storage.getContainer(Predicate).queryLast(name=pred) for k, v in crit.items: if isinstance(v, Track): crit[k] = v.uid - rels = self.storage.getContainer('rel') + rels = self.storage.getContainer(Triple) return rels.query(**crit) @@ -55,7 +55,7 @@ class Predicates(Concepts): defaultPredicate = 'standard' def storePredicate(storage, name): - preds = storage.getContainer('pred') + preds = storage.getContainer(Predicate) preds.save(Predicate(name)) @@ -92,12 +92,16 @@ class Type(Concept): headFields = ['name', 'tprefix'] prefix = 'type' + @property + def typeClass(self): + return registry[self.tprefix].itemFactory + def values(self): - cont = self.container.storage.getContainer(self.tprefix) + cont = self.container.storage.getContainer(self.typeClass) return cont.query() def get(self, key, default=None): - cont = self.container.storage.getContainer(self.tprefix) + cont = self.container.storage.getContainer(self.typeClass) return cont.queryLast(name=key) or default def __getitem__(self, key): @@ -107,7 +111,7 @@ class Type(Concept): return value def __setitem__(self, key, value): - cont = self.container.storage.getContainer(self.tprefix) + cont = self.container.storage.getContainer(self.typeClass) value.name = key cont.save(value) @@ -121,7 +125,7 @@ class Types(Concepts): def storeType(storage, cls, name): - types = storage.getContainer('type') + types = storage.getContainer(Type) types.save(Type(name, cls.prefix)) def setupCoreTypes(storage): diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 3a839bc..0cab6e0 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -79,7 +79,7 @@ def test_type(self, config): storage.dropTable('types') concept.setupCoreTypes(storage) - types = storage.getContainer(concept.Type.prefix) + types = storage.getContainer(concept.Type) tps = list(types.query()) self.assertEqual(len(tps), 6) self.assertEqual(tps[0].name, 'track') @@ -95,7 +95,7 @@ def test_type(self, config): def test_topic(self, config): storage = config.storageFactory(config.dbschema) storage.dropTable('topics') - topics = storage.getContainer(topic.Topic.prefix) + topics = storage.getContainer(topic.Topic) concept.storePredicate(storage, concept.defaultPredicate) root = folder.Root(storage) root['top']['topics'] = folder.Folder() From dfc5cefcee8f3c6e2a17d9efe5c37da2983a2cee Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 16 Mar 2024 14:26:36 +0100 Subject: [PATCH 17/20] fixes; work in progress: topics --- scopes/storage/folder.py | 4 ++-- scopes/storage/tracking.py | 15 +++++++++++---- tests/tlib_storage.py | 10 ++++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/scopes/storage/folder.py b/scopes/storage/folder.py index 1a05e61..fd3994c 100644 --- a/scopes/storage/folder.py +++ b/scopes/storage/folder.py @@ -45,8 +45,8 @@ class Folder(Track): return self.container.storage.getItem(self.ref) def setTarget(self, target): - self.ref = target.uid - self.container.save(self) + self.set('ref', target.uid) + self.container.update(self) def __str__(self): return '%s: %s; keys: %s' % (self.__class__.__name__, diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index e2ebe59..ebd45ba 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -119,10 +119,16 @@ class Container(object): return self.insert(track) if self.insertOnChange and found.data != track.data: return self.insert(track) - if found.data != track.data or found.timeStamp != track.timeStamp: + changed = False + if found.data != track.data: found.update(track.data) + changed = True + if track.timeStamp is not None and found.timeStamp != track.timeStamp: found.timeStamp = track.timeStamp + changed = True + if changed: self.update(found) + track.trackId = found.trackId return found.trackId def insert(self, track, withTrackId=False): @@ -130,14 +136,15 @@ class Container(object): values = self.setupValues(track, withTrackId) stmt = t.insert().values(**values).returning(t.c.trackid) trackId = self.session.execute(stmt).first()[0] + track.trackId = trackId self.storage.mark_changed() return trackId - def update(self, track): + def update(self, track, updateTimeStamp=False): t = self.table + if updateTimeStamp or track.timeStamp is None: + track.timeStamp = datetime.now() values = self.setupValues(track) - if track.timeStamp is None: - values['timestamp'] = datetime.now() stmt = t.update().values(**values).where(t.c.trackid == track.trackId) n = self.session.execute(stmt).rowcount if n > 0: diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 0cab6e0..b6526e6 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -96,11 +96,17 @@ def test_topic(self, config): storage = config.storageFactory(config.dbschema) storage.dropTable('topics') topics = storage.getContainer(topic.Topic) + types = storage.getContainer(concept.Type) concept.storePredicate(storage, concept.defaultPredicate) root = folder.Root(storage) - root['top']['topics'] = folder.Folder() + root['top']['topics'] = ftopics = folder.Folder() + ttopic = types.queryLast(name='topic') + self.assertEqual(ttopic.name, 'topic') + ftopics.setTarget(ttopic) + self.assertEqual(ftopics.ref, 'type-6') - tp_itc = topic.Topic('itc') + tp_itc = topic.Topic('itc', data=dict( + title='ITC', description='Information and Communication Technology')) topics.save(tp_itc) storage.commit() From e676e10e333a893afb400d4523eadb95db025689 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 16 Mar 2024 15:58:00 +0100 Subject: [PATCH 18/20] work in progress: relationships --- scopes/storage/concept.py | 8 ++++++-- scopes/storage/tracking.py | 6 +++++- tests/tlib_storage.py | 5 +++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index 26b37db..774bf60 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -7,6 +7,8 @@ from scopes.interfaces import IContainer from scopes.storage.common import registerContainerClass, registry from scopes.storage.tracking import Container, Track +defaultPredicate = 'standard' + class Concept(Track): @@ -21,6 +23,10 @@ class Concept(Track): def values(self): return (t.getSecond() for t in self.children(Rels.defaultPredicate)) + def addChild(self, child, predicate = defaultPredicate): + rels = self.container.storage.getContainer(Triple) + rels.save(Triple(self.uid, child.uid, predicate)) + class Concepts(Container): @@ -52,8 +58,6 @@ class Predicates(Concepts): tableName = 'preds' -defaultPredicate = 'standard' - def storePredicate(storage, name): preds = storage.getContainer(Predicate) preds.save(Predicate(name)) diff --git a/scopes/storage/tracking.py b/scopes/storage/tracking.py index ebd45ba..1134b86 100644 --- a/scopes/storage/tracking.py +++ b/scopes/storage/tracking.py @@ -20,8 +20,12 @@ class Track(object): headFields = ['taskId', 'userName'] prefix = 'rec' - def __init__(self, *keys, data=None, timeStamp=None, trackId=None, container=None): + def __init__(self, *keys, data=None, timeStamp=None, trackId=None, + container=None, **kw): self.head = {} + for k, v in kw.items(): + if k in self.headFields: + self.head[k] = kw.pop(k) for ix, k in enumerate(keys): self.head[self.headFields[ix]] = k for k in self.headFields: diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index b6526e6..4d3ae3e 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -108,6 +108,11 @@ def test_topic(self, config): tp_itc = topic.Topic('itc', data=dict( title='ITC', description='Information and Communication Technology')) topics.save(tp_itc) + tp_proglang = topic.Topic('prog_lang', data=dict( + title='Programming Languages', + description='Programming Languages')) + topics.save(tp_proglang) + tp_itc.addChild(tp_proglang) storage.commit() From 13706c3d840d9bd6d9e895ae0a5f6998ae8b92e9 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 16 Mar 2024 18:03:32 +0100 Subject: [PATCH 19/20] relationship stuff basically working --- scopes/storage/concept.py | 8 ++++---- tests/tlib_storage.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index 774bf60..f8f37c4 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -34,10 +34,10 @@ class Concepts(Container): indexes = None def queryRels(self, **crit): - pred = crit.get(predicate) - if pred is not None and isinstance(pred, ('string', 'bytes')): - crit['predicate'] = self.storage.getContainer(Predicate).queryLast(name=pred) - for k, v in crit.items: + #pred = crit.get(predicate) + #if pred is not None and isinstance(pred, ('string', 'bytes')): + # crit['predicate'] = self.storage.getContainer(Predicate).queryLast(name=pred) + for k, v in crit.items(): if isinstance(v, Track): crit[k] = v.uid rels = self.storage.getContainer(Triple) diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 4d3ae3e..9ce54e5 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -114,5 +114,8 @@ def test_topic(self, config): topics.save(tp_proglang) tp_itc.addChild(tp_proglang) + c = list(tp_itc.children()) + self.assertEqual(c[0].getSecond().name, 'prog_lang') + storage.commit() From 33b1cbe823ec6e1810dfc5c8aeb7605f3724f887 Mon Sep 17 00:00:00 2001 From: Helmut Merz Date: Sat, 16 Mar 2024 23:38:58 +0100 Subject: [PATCH 20/20] more on children() and parents() --- scopes/storage/concept.py | 10 ++++++++-- tests/tlib_storage.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scopes/storage/concept.py b/scopes/storage/concept.py index f8f37c4..33a8e2f 100644 --- a/scopes/storage/concept.py +++ b/scopes/storage/concept.py @@ -15,15 +15,21 @@ class Concept(Track): headFields = ['name'] def parents(self, predicate=None): + return (r.getFirst() for r in self.parentRels(predicate)) + + def parentRels(self, predicate=None): return self.container.queryRels(second=self, predicate=predicate) def children(self, predicate=None): + return (r.getSecond() for r in self.childRels(predicate)) + + def childRels(self, predicate=None): return self.container.queryRels(first=self, predicate=predicate) def values(self): - return (t.getSecond() for t in self.children(Rels.defaultPredicate)) + return self.children(defaultPredicate) - def addChild(self, child, predicate = defaultPredicate): + def addChild(self, child, predicate=defaultPredicate): rels = self.container.storage.getContainer(Triple) rels.save(Triple(self.uid, child.uid, predicate)) diff --git a/tests/tlib_storage.py b/tests/tlib_storage.py index 9ce54e5..fcb378c 100644 --- a/tests/tlib_storage.py +++ b/tests/tlib_storage.py @@ -115,7 +115,7 @@ def test_topic(self, config): tp_itc.addChild(tp_proglang) c = list(tp_itc.children()) - self.assertEqual(c[0].getSecond().name, 'prog_lang') + self.assertEqual(c[0].name, 'prog_lang') storage.commit()