Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8b0ce6f
fix: fall back to diskcache when memcached is inaccessible
sknoxnexsys Jun 23, 2026
249fec6
refactor: remove zope.sqlalchemy dependency
sknoxnexsys Jun 23, 2026
114e737
fix: correct rule and template management functions
sknoxnexsys Jun 23, 2026
6eb213f
perf: speed up bulk resource-attribute and scenario-data insertion
sknoxnexsys Jun 23, 2026
56ac61b
feat: add DB migrations for cloned_network_id and cloned_project_id c…
sknoxnexsys Jun 23, 2026
34431cb
Merge branch 'SK_fixes' of ssh://github.com/hydraplatform/hydra-base …
sknoxnexsys Jun 23, 2026
f98319d
fix diverged alembic heads
sknoxnexsys Jun 23, 2026
e92deb0
fix: Improve timing of the get_all_attributes_in_network by separatin…
sknoxnexsys Jun 24, 2026
8f20a75
Update the get_projects folder to include any networks contained in n…
sknoxnexsys Jul 8, 2026
e3e00dd
Wrap dataset in StringIO when reading json
sknoxnexsys Jul 8, 2026
dc04710
Merge branch 'SK_fixes' of ssh://github.com/hydraplatform/hydra-base …
Jul 9, 2026
7097fb4
Merge branch 'master' into SK_fixes
sknoxnexsys Jul 9, 2026
497e0e6
Merge branch 'SK_fixes' of ssh://github.com/hydraplatform/hydra-base …
sknoxnexsys Jul 9, 2026
11a9885
Add TTL to check_perm's per-thread permission cache
sknoxnexsys Jul 9, 2026
5c41973
Harden bulk_update_resourcedata's batch fast lane
sknoxnexsys Jul 9, 2026
9960877
Merge branch 'SK_fixes' of origin into SK_fixes
sknoxnexsys Jul 9, 2026
843eac9
Fix alembic head divergence from merged node_alt_coords migration
sknoxnexsys Jul 9, 2026
feecc79
fix: catch memcached errors in build_user_cache cache check
sknoxnexsys Jul 9, 2026
6874f61
Add a function and associated test to update the appdata on a network
sknoxnexsys Jul 13, 2026
a4e233a
Add an update_appdata function for a project and the associated test
sknoxnexsys Jul 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions hydra_base/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@
from sqlalchemy.engine import Engine

from .. import config
from zope.sqlalchemy import register

from hydra_base.exceptions import HydraError

import transaction
from sqlalchemy.orm import sessionmaker, declarative_base

import logging
Expand Down Expand Up @@ -157,7 +154,6 @@ def connect(db_url=None):

maker = sessionmaker(bind=engine, autoflush=False, autocommit=False)
DBSession = scoped_session(maker)
register(DBSession)

global DeclarativeBase
try:
Expand All @@ -173,10 +169,10 @@ def get_session():

def commit_transaction():
try:
transaction.commit()
DBSession.commit()
except Exception as e:
log.critical(e)
transaction.abort()
DBSession.rollback()

def open_session():
log.debug("OPENING SESSION")
Expand All @@ -198,8 +194,37 @@ def close_session():


def rollback_transaction():
#import pudb; pudb.set_trace()
transaction.abort()
DBSession.rollback()

def bulk_insert_ignore(model, rows):
"""
Bulk insert rows into model, silently skipping any that would violate a
unique constraint. Cross-database compatible.

Does not return inserted IDs — query back as needed after calling.
"""
if not rows:
return

if engine is None:
raise HydraError("bulk_insert_ignore: No database engine available. Please call connect() first.")

dialect_name = engine.dialect.name

if dialect_name == 'mysql':
from sqlalchemy.dialects.mysql import insert as _insert
stmt = _insert(model).values(rows).prefix_with('IGNORE')
elif dialect_name == 'postgresql':
from sqlalchemy.dialects.postgresql import insert as _insert
stmt = _insert(model).values(rows).on_conflict_do_nothing()
elif dialect_name == 'sqlite':
from sqlalchemy.dialects.sqlite import insert as _insert
stmt = _insert(model).values(rows).on_conflict_do_nothing()
else:
raise HydraError(f"bulk_insert_ignore: unsupported dialect '{dialect_name}'")

DBSession.execute(stmt)


def restart_session(caller='-- not specified --'):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

# revision identifiers, used by Alembic.
revision = 'a81a860cda39'
down_revision = '04e4ae80b7b9'
down_revision = 'cec2b77ad85e'
branch_labels = None
depends_on = None

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""node_alt_coords

Revision ID: b7f3e1a92c44
Revises: a1b2c3d4e5f6
Revises: edf7bffb7b33
Create Date: 2026-07-09 00:00:00.000000

"""
Expand All @@ -13,7 +13,7 @@

# revision identifiers, used by Alembic.
revision = 'b7f3e1a92c44'
down_revision = 'a1b2c3d4e5f6'
down_revision = 'edf7bffb7b33'
branch_labels = None
depends_on = None

Expand Down
38 changes: 38 additions & 0 deletions hydra_base/db/alembic/versions/d4e9b1f2c83a_cloned_network_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""cloned_network_id

Revision ID: d4e9b1f2c83a
Revises: a81a860cda39
Create Date: 2026-05-20 00:00:00.000000

"""
import logging
from alembic import op
import sqlalchemy as sa

log = logging.getLogger(__name__)

# revision identifiers, used by Alembic.
revision = 'd4e9b1f2c83a'
down_revision = 'a81a860cda39'
branch_labels = None
depends_on = None


def upgrade():
if op.get_bind().dialect.name == 'mysql':
try:
op.add_column('tNetwork',
sa.Column('cloned_network_id',
sa.Integer(),
sa.ForeignKey('tNetwork.id'),
nullable=True))
except Exception as e:
log.critical(e)


def downgrade():
if op.get_bind().dialect.name == 'mysql':
try:
op.drop_column('tNetwork', 'cloned_network_id')
except Exception as e:
log.critical(e)
38 changes: 38 additions & 0 deletions hydra_base/db/alembic/versions/e7a3c9f04b12_cloned_project_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""cloned_project_id

Revision ID: e7a3c9f04b12
Revises: d4e9b1f2c83a
Create Date: 2026-05-20 00:00:00.000000

"""
import logging
from alembic import op
import sqlalchemy as sa

log = logging.getLogger(__name__)

# revision identifiers, used by Alembic.
revision = 'e7a3c9f04b12'
down_revision = 'd4e9b1f2c83a'
branch_labels = None
depends_on = None


def upgrade():
if op.get_bind().dialect.name == 'mysql':
try:
op.add_column('tProject',
sa.Column('cloned_project_id',
sa.Integer(),
sa.ForeignKey('tProject.id'),
nullable=True))
except Exception as e:
log.critical(e)


def downgrade():
if op.get_bind().dialect.name == 'mysql':
try:
op.drop_column('tProject', 'cloned_project_id')
except Exception as e:
log.critical(e)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""merge divergent heads

Revision ID: edf7bffb7b33
Revises: 580425ade2e4, 877adf863b33, a1b2c3d4e5f6
Create Date: 2026-06-23 16:06:15.787461

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'edf7bffb7b33'
down_revision = ('580425ade2e4', '877adf863b33', 'a1b2c3d4e5f6')
branch_labels = None
depends_on = None


def upgrade():
pass


def downgrade():
pass
23 changes: 23 additions & 0 deletions hydra_base/db/model/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with HydraPlatform. If not, see <http://www.gnu.org/licenses/>
#
import uuid

from .base import *

from hydra_base.lib.storage import (
Expand Down Expand Up @@ -160,6 +162,27 @@ def set_hash(self,metadata=None):

return data_hash

def set_unique_hash(self, metadata=None):
"""
Like set_hash(), but guarantees the result cannot collide with any
other dataset's hash. Used when a hash collision was found but the
existing dataset can't be reused (e.g. no read permission on it) --
tDataset.hash has a DB-level UNIQUE constraint, so leaving the hash
as a duplicate would raise IntegrityError on flush.

The salt is folded into the hash computation only -- it is NOT
passed to set_metadata()/persisted as a real metadata row, since
that would leak an internal implementation detail into the
dataset's actual (user-visible) metadata.
"""
if metadata is None:
metadata = self.get_metadata_as_dict()

salted_metadata = dict(metadata)
salted_metadata['_hash_salt'] = uuid.uuid4().hex

return self.set_hash(metadata=salted_metadata)

def get_metadata_as_dict(self):
metadata = {}
sortedmeta = sorted(self.metadata, key=lambda x:x.key.lower())
Expand Down
7 changes: 5 additions & 2 deletions hydra_base/db/model/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,11 @@ def build_user_cache(cls, uid):
Build the cache of projects a user has access to either by direct Ownership
or by indirect access required for navigating to a project to which they own
"""
if cache.get(_user_project_cache_key(uid)) is not None:
return
try:
if cache.get(_user_project_cache_key(uid)) is not None:
return
except Exception as e:
log.warning(f"Error checking project cache for user {uid}: {e}")

user_cache = defaultdict(list)
projects_qry = get_session().query(Project)
Expand Down
7 changes: 6 additions & 1 deletion hydra_base/db/model/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,14 @@ def asdict(self):
"id": self.id,
"name": self.name,
"value": self.value,
"format": self.format,
"ref_key": self.ref_key,
"network_id": self.network_id,
"template_id": self.template_id,
"description": self.description,
"status": self.status,
"owners": self.owners
"owners": self.owners,
"types": [{"code": t.code} for t in self.types]
}


Expand Down
10 changes: 9 additions & 1 deletion hydra_base/db/model/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# along with HydraPlatform. If not, see <http://www.gnu.org/licenses/>
#
from .base import *
from .base import _is_admin

__all__ = ['Template', 'TemplateType', 'TypeAttr', 'ResourceType', 'ProjectTemplate']

Expand Down Expand Up @@ -323,6 +324,13 @@ def get_hierarchy(self, user_id):
hierarchy = hierarchy + self.parent.get_hierarchy(user_id)
return hierarchy

def check_write_permission(self, user_id, do_raise=True):
if _is_admin(user_id):
return True
if do_raise:
raise PermissionError("Permission denied. User %s does not have edit"
" access on template %s" % (user_id, self.id))
return False

class TemplateType(Base, Inspect):
"""
Expand Down Expand Up @@ -660,4 +668,4 @@ class ProjectTemplate(Base, Inspect, PermissionControlled):
cr_date = Column(TIMESTAMP(), nullable=False, server_default=text(u'CURRENT_TIMESTAMP'))

_parents = ['tProject', 'tTemplate']
_children = []
_children = []
Loading
Loading