merge branch master, fix tests for using zope testrunner, with postgres only

This commit is contained in:
Helmut Merz 2024-03-09 19:26:18 +01:00
commit 0fb0ba0c74
13 changed files with 275 additions and 43 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@
*#*# *#*#
*.#* *.#*
__pycache__ __pycache__
var

View file

@ -0,0 +1 @@
"""package scopes.organize"""

13
scopes/organize/task.py Normal file
View file

@ -0,0 +1,13 @@
# scopes.organize.task
"""Task (and corresponding container) implementation."""
from scopes.storage.common import registerContainerClass
from scopes.storage.concept import Concept
class Task(Concept):
headFields = ['name']
prefix = 'tsk'

View file

@ -0,0 +1 @@
"""package scopes.server"""

44
scopes/server/app.py Normal file
View file

@ -0,0 +1,44 @@
# scopes.server.app
from zope.publisher.base import DefaultPublication
from zope.publisher.browser import BrowserRequest
from zope.publisher.publish import publish
from zope.traversing.publicationtraverse import PublicationTraverser
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(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()
class AppRoot:
"""Zope Demo AppRoot"""
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'

View file

@ -4,35 +4,61 @@
import base64 import base64
from sqlalchemy import create_engine, MetaData, text from sqlalchemy import create_engine, MetaData, text
from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy import Integer
from sqlalchemy.dialects.sqlite import JSON
import threading import threading
import zope.sqlalchemy
class StorageFactory(object):
def sessionFactory(self):
return self.engine.connect
@staticmethod
def getEngine(dbtype, dbname, user, pw, host='localhost', port=5432, **kw): def getEngine(dbtype, dbname, user, pw, host='localhost', port=5432, **kw):
return create_engine('%s://%s:%s@%s:%s/%s' % ( return create_engine('%s:///%s' % (dbtype, dbname), **kw)
dbtype, user, pw, host, port, dbname), **kw)
def sessionFactory(engine): @staticmethod
Session = scoped_session(sessionmaker(bind=engine, twophase=True)) def mark_changed(session):
zope.sqlalchemy.register(Session) pass
return Session
# put something like this in code before first creating a Storage object @staticmethod
#engine = getEngine('postgresql+psycopg', 'testdb', 'testuser', 'secret') def commit(conn):
#scopes.storage.common.engine = engine conn.commit()
#scopes.storage.common.Session = sessionFactory(engine)
IdType = Integer
JsonType = JSON
def __init__(self, config):
self.engine = self.getEngine(config.dbengine, config.dbname,
config.dbuser, config.dbpassword)
self.Session = self.sessionFactory()
def __call__(self, schema=None):
return Storage(self, schema=schema)
# you may put something like this in your code:
#factory = StorageFactory(config)
# and then call at appropriate places:
#storage = scopes.storage.common.factory(schema=...)
class Storage(object): class Storage(object):
def __init__(self, schema=None): def __init__(self, db, schema=None):
self.engine = engine self.db = db
self.session = Session() self.engine = db.engine
self.session = db.Session()
self.schema = schema self.schema = schema
self.metadata = MetaData(schema=schema) self.metadata = MetaData(schema=schema)
self.containers = {} self.containers = {}
def commit(self):
self.db.commit(self.session)
def mark_changed(self):
self.db.mark_changed(self.session)
def create(self, cls): def create(self, cls):
container = cls(self) container = cls(self)
self.add(container) self.add(container)
@ -57,7 +83,8 @@ class Storage(object):
def dropTable(self, tableName): def dropTable(self, tableName):
with self.engine.begin() as conn: with self.engine.begin() as conn:
conn.execute(text('drop table if exists %s.%s' % (self.schema, tableName))) prefix = self.schema and self.schema + '.' or ''
conn.execute(text('drop table if exists %s%s' % (prefix, tableName)))
def resetSequence(self, tableName, colName, v): def resetSequence(self, tableName, colName, v):
sq = ('alter sequence %s.%s_%s_seq restart %i' % sq = ('alter sequence %s.%s_%s_seq restart %i' %
@ -71,8 +98,10 @@ class Storage(object):
registry = {} registry = {}
def registerContainerClass(cls): def registerContainerClass(cls):
# TODO: error on duplicate key prefix = cls.itemFactory.prefix
registry[cls.itemFactory.prefix] = cls if prefix in registry:
raise ValueError("prefix '%s' already registered!" % prefix)
registry[prefix] = cls
cls.headCols = cols = tuple(f.lower() for f in cls.itemFactory.headFields) cls.headCols = cols = tuple(f.lower() for f in cls.itemFactory.headFields)
if cls.indexes is None: if cls.indexes is None:
cls.indexes = [cols[i:] for i in range(len(cols))] cls.indexes = [cols[i:] for i in range(len(cols))]

12
scopes/storage/concept.py Normal file
View file

@ -0,0 +1,12 @@
# scopes.storage.concept
"""Abstract base classes for concept map application classes."""
from scopes.storage.common import registerContainerClass
from scopes.storage.tracking import Container, Track
class Concept(Track):
headFields = ['name']

View file

@ -0,0 +1 @@
"""scopes.storage.db"""

View file

@ -0,0 +1,37 @@
# scopes.storage.db.postgres
"""Database-related code specific for PostgreSQL."""
from sqlalchemy import create_engine
from sqlalchemy import BigInteger
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import scoped_session, sessionmaker
import transaction
from zope.sqlalchemy import register, mark_changed
from scopes.storage.common import StorageFactory
class StorageFactory(StorageFactory):
def sessionFactory(self):
Session = scoped_session(sessionmaker(bind=self.engine, twophase=True))
register(Session)
return Session
@staticmethod
def getEngine(dbtype, dbname, user, pw, host='localhost', port=5432, **kw):
return create_engine('%s://%s:%s@%s:%s/%s' % (
dbtype, user, pw, host, port, dbname), **kw)
@staticmethod
def mark_changed(session):
return mark_changed(session)
@staticmethod
def commit(conn):
transaction.commit()
IdType = BigInteger
JsonType = JSONB

51
scopes/storage/folder.py Normal file
View file

@ -0,0 +1,51 @@
# scopes.storage.folder
from scopes.storage.common import registerContainerClass
from scopes.storage.tracking import Container, Track
class Folder(Track):
headFields = ['parent', 'name', 'ref']
prefix = 'fldr'
def keys(self):
for f in self.container.query(parent=self.rid):
yield f.name
def get(self, key, default=None):
value = self.container.queryLast(parent=self.rid, name=key)
if value is None:
return default
return value
def __getitem__(self, key):
value = self.container.queryLast(parent=self.rid, name=key)
if value is None:
raise KeyError(key)
return value
def __setitem__(self, key, value):
value.set('parent', self.rid)
value.set('name', key)
self.container.save(value)
class Root(Folder):
"""A dummy (virtual) root folder for creating real folders
using the Folder API."""
def __init__(self, storage):
cont = storage.create(Folders)
super(Root, self).__init__(container=cont)
uid = ''
@registerContainerClass
class Folders(Container):
itemFactory = Folder
indexes = [('parent', 'name'), ('ref',)]
tableName = 'folders'
insertOnChange = False

View file

@ -0,0 +1,13 @@
# scopes.storage.relation
"""An SQL-based relationship engine using RDF-like triples."""
from scopes.storage.common import registerContainerClass
from scopes.storage.tracking import Container, Track
class Triple(Track):
headFields = ['first', 'second', 'pred']
prefix = 'rel'

View file

@ -9,11 +9,8 @@ data (payload) represented as a dict.
import base64 import base64
from datetime import datetime from datetime import datetime
from sqlalchemy import Table, Column, Index from sqlalchemy import Table, Column, Index
from sqlalchemy import BigInteger, DateTime, Text, func from sqlalchemy import DateTime, Text, func
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.dialects.postgresql import JSONB
import transaction
from zope.sqlalchemy import register, mark_changed
from scopes.storage.common import registerContainerClass from scopes.storage.common import registerContainerClass
@ -36,6 +33,15 @@ class Track(object):
self.trackId = kw.get('trackId') self.trackId = kw.get('trackId')
self.container = kw.get('container') self.container = kw.get('container')
def set(self, attr, value):
if attr in self.headFields:
if value is None:
value = ''
self.head[attr] = value
setattr(self, attr, value)
else:
raise AttributeError(attr)
def update(self, data, overwrite=False): def update(self, data, overwrite=False):
if data is None: if data is None:
return return
@ -50,6 +56,12 @@ class Track(object):
return None return None
return '%s-%d' % (self.prefix, self.trackId) return '%s-%d' % (self.prefix, self.trackId)
@property
def rid(self):
if self.trackId is None:
return ''
return str(self.trackId)
@registerContainerClass @registerContainerClass
class Container(object): class Container(object):
@ -64,6 +76,7 @@ class Container(object):
def __init__(self, storage): def __init__(self, storage):
self.storage = storage self.storage = storage
self.db = storage.db
self.session = storage.session self.session = storage.session
self.table = self.getTable() self.table = self.getTable()
@ -100,7 +113,7 @@ class Container(object):
values = self.setupValues(track, withTrackId) values = self.setupValues(track, withTrackId)
stmt = t.insert().values(**values).returning(t.c.trackid) stmt = t.insert().values(**values).returning(t.c.trackid)
trackId = self.session.execute(stmt).first()[0] trackId = self.session.execute(stmt).first()[0]
mark_changed(self.session) self.storage.mark_changed()
return trackId return trackId
def update(self, track): def update(self, track):
@ -111,7 +124,7 @@ class Container(object):
stmt = t.update().values(**values).where(t.c.trackid == track.trackId) stmt = t.update().values(**values).where(t.c.trackid == track.trackId)
n = self.session.execute(stmt).rowcount n = self.session.execute(stmt).rowcount
if n > 0: if n > 0:
mark_changed(self.session) self.storage.mark_changed()
return n return n
def upsert(self, track): def upsert(self, track):
@ -129,7 +142,7 @@ class Container(object):
stmt = self.table.delete().where(self.table.c.trackid == trackId) stmt = self.table.delete().where(self.table.c.trackid == trackId)
n = self.session.execute(stmt).rowcount n = self.session.execute(stmt).rowcount
if n > 0: if n > 0:
mark_changed(self.session) self.storage.mark_changed()
return n return n
def makeTrack(self, r): def makeTrack(self, r):
@ -162,7 +175,7 @@ class Container(object):
def createTable(storage, tableName, headcols, indexes=None): def createTable(storage, tableName, headcols, indexes=None):
metadata = storage.metadata metadata = storage.metadata
cols = [Column('trackid', BigInteger, primary_key=True)] cols = [Column('trackid', storage.db.IdType, primary_key=True)]
idxs = [] idxs = []
for ix, f in enumerate(headcols): for ix, f in enumerate(headcols):
cols.append(Column(f.lower(), Text, nullable=False, server_default='')) cols.append(Column(f.lower(), Text, nullable=False, server_default=''))
@ -172,7 +185,7 @@ def createTable(storage, tableName, headcols, indexes=None):
indexName = 'idx_%s_%d' % (tableName, (ix + 1)) indexName = 'idx_%s_%d' % (tableName, (ix + 1))
idxs.append(Index(indexName, *idef)) idxs.append(Index(indexName, *idef))
idxs.append(Index('idx_%s_ts' % tableName, 'timestamp')) idxs.append(Index('idx_%s_ts' % tableName, 'timestamp'))
cols.append(Column('data', JSONB, nullable=False, server_default='{}')) cols.append(Column('data', storage.db.JsonType, nullable=False, server_default='{}'))
table = Table(tableName, metadata, *(cols+idxs), extend_existing=True) table = Table(tableName, metadata, *(cols+idxs), extend_existing=True)
metadata.create_all(storage.engine) metadata.create_all(storage.engine)
return table return table

View file

@ -1,27 +1,28 @@
#! /usr/bin/python """The real test implementations"""
"""Tests for the 'scopes.storage' package."""
from datetime import datetime from datetime import datetime
import transaction
import unittest import unittest
import scopes.storage.common from scopes.storage import folder, tracking
from scopes.storage.common import Storage, getEngine, sessionFactory
from scopes.storage import proxy
from scopes.storage import tracking
engine = getEngine('postgresql', 'ccotest', 'ccotest', 'cco') #import config
scopes.storage.common.engine = engine class Config(object): pass
scopes.storage.common.Session = sessionFactory(engine)
storage = Storage(schema='testing') config = Config()
config.dbengine = 'postgresql'
config.dbname = 'ccotest'
config.dbuser = 'ccotest'
config.dbpassword = 'cco'
# PostgreSQL-specific settings
from scopes.storage.db.postgres import StorageFactory
factory = StorageFactory(config)
storage = factory(schema='testing')
class Test(unittest.TestCase): class Test(unittest.TestCase):
"Basic tests for the cco.storage package."
def testBasicStuff(self): def test_001_tracking(self):
storage.dropTable('tracks') storage.dropTable('tracks')
tracks = storage.create(tracking.Container) tracks = storage.create(tracking.Container)
@ -67,7 +68,22 @@ class Test(unittest.TestCase):
self.assertEqual(n, 1) self.assertEqual(n, 1)
self.assertEqual(tracks.get(31), None) self.assertEqual(tracks.get(31), None)
transaction.commit() storage.commit()
def test_002_folder(self):
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()
def suite(): def suite():