bluecore-models 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bluecore_models-0.2.0/PKG-INFO +56 -0
- bluecore_models-0.2.0/README.md +46 -0
- bluecore_models-0.2.0/pyproject.toml +21 -0
- bluecore_models-0.2.0/setup.cfg +4 -0
- bluecore_models-0.2.0/src/bluecore/models/__init__.py +13 -0
- bluecore_models-0.2.0/src/bluecore/models/base.py +3 -0
- bluecore_models-0.2.0/src/bluecore/models/bf_classes.py +49 -0
- bluecore_models-0.2.0/src/bluecore/models/instance.py +30 -0
- bluecore_models-0.2.0/src/bluecore/models/other_resource.py +66 -0
- bluecore_models-0.2.0/src/bluecore/models/resource.py +32 -0
- bluecore_models-0.2.0/src/bluecore/models/version.py +35 -0
- bluecore_models-0.2.0/src/bluecore/models/work.py +25 -0
- bluecore_models-0.2.0/src/bluecore/utils/graph.py +88 -0
- bluecore_models-0.2.0/src/bluecore_models.egg-info/PKG-INFO +56 -0
- bluecore_models-0.2.0/src/bluecore_models.egg-info/SOURCES.txt +18 -0
- bluecore_models-0.2.0/src/bluecore_models.egg-info/dependency_links.txt +1 -0
- bluecore_models-0.2.0/src/bluecore_models.egg-info/requires.txt +3 -0
- bluecore_models-0.2.0/src/bluecore_models.egg-info/top_level.txt +1 -0
- bluecore_models-0.2.0/tests/test_models.py +198 -0
- bluecore_models-0.2.0/tests/test_utils.py +36 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: bluecore-models
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Blue Core BIBFRAME Data Models
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: alembic>=1.14.1
|
|
8
|
+
Requires-Dist: psycopg2-binary>=2.9.10
|
|
9
|
+
Requires-Dist: rdflib>=7.1.3
|
|
10
|
+
|
|
11
|
+
# Blue Core Data Models
|
|
12
|
+
The Blue Core Data Models are used in [Blue Core API](https://github.com/blue-core-lod/bluecore_api)
|
|
13
|
+
and in the [Blue Core Workflows](https://github.com/blue-core-lod/bluecore-workflows) services.
|
|
14
|
+
|
|
15
|
+
## Run Postgres with Docker
|
|
16
|
+
To run the Postgres with the Blue Core Database, run the following command from this directory:
|
|
17
|
+
|
|
18
|
+
`docker run --name bluecore_db -e POSTGRES_USER=bluecore_admin -e POSTGRES_PASSWORD=bluecore_admin -v ./create-db.sql:/docker-entrypoint-initdb.d/create_database.sql -p 5432:5432 postgres:17`
|
|
19
|
+
|
|
20
|
+
## Installing
|
|
21
|
+
- Install via pip: `pip install blue-core-data-models`
|
|
22
|
+
- Install via uv: `uv add blue-core-data-models`
|
|
23
|
+
|
|
24
|
+
## Database Management
|
|
25
|
+
The [SQLAlchemy](https://www.sqlalchemy.org/) Object Relational Mapper (ORM) is used to create
|
|
26
|
+
the Bluecore database models.
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
erDiagram
|
|
30
|
+
ResourceBase ||--o{ Instance : "has"
|
|
31
|
+
ResourceBase ||--o{ Work : "has"
|
|
32
|
+
ResourceBase ||--o{ OtherResource : "has"
|
|
33
|
+
ResourceBase ||--o{ ResourceBibframeClass : "has classes"
|
|
34
|
+
ResourceBase ||--o{ Version : "has versions"
|
|
35
|
+
ResourceBase ||--o{ BibframeOtherResources : "has other resources"
|
|
36
|
+
|
|
37
|
+
Work ||--o{ Instance : "has"
|
|
38
|
+
|
|
39
|
+
BibframeClass ||--o{ ResourceBibframeClass : "classifies"
|
|
40
|
+
|
|
41
|
+
OtherResource ||--o{ BibframeOtherResources : "links to"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Database Migrations with Alembic
|
|
45
|
+
The [Alembic](https://alembic.sqlalchemy.org/en/latest/) database migration package is used
|
|
46
|
+
to manage database changes with the Bluecore Data models.
|
|
47
|
+
|
|
48
|
+
To create a new migration, ensure that the Postgres database is available and then run:
|
|
49
|
+
- `uv run alembic revision --autogenerate -m "{short message describing change}`
|
|
50
|
+
|
|
51
|
+
A new migration script will be created in the `bluecore_store_migration` directory. Be sure
|
|
52
|
+
to add the new script to the repository with `git`.
|
|
53
|
+
|
|
54
|
+
#### Applying Migrations
|
|
55
|
+
To apply all of the migrations, run the following command:
|
|
56
|
+
- `uv run alembic upgrade head`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Blue Core Data Models
|
|
2
|
+
The Blue Core Data Models are used in [Blue Core API](https://github.com/blue-core-lod/bluecore_api)
|
|
3
|
+
and in the [Blue Core Workflows](https://github.com/blue-core-lod/bluecore-workflows) services.
|
|
4
|
+
|
|
5
|
+
## Run Postgres with Docker
|
|
6
|
+
To run the Postgres with the Blue Core Database, run the following command from this directory:
|
|
7
|
+
|
|
8
|
+
`docker run --name bluecore_db -e POSTGRES_USER=bluecore_admin -e POSTGRES_PASSWORD=bluecore_admin -v ./create-db.sql:/docker-entrypoint-initdb.d/create_database.sql -p 5432:5432 postgres:17`
|
|
9
|
+
|
|
10
|
+
## Installing
|
|
11
|
+
- Install via pip: `pip install blue-core-data-models`
|
|
12
|
+
- Install via uv: `uv add blue-core-data-models`
|
|
13
|
+
|
|
14
|
+
## Database Management
|
|
15
|
+
The [SQLAlchemy](https://www.sqlalchemy.org/) Object Relational Mapper (ORM) is used to create
|
|
16
|
+
the Bluecore database models.
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
erDiagram
|
|
20
|
+
ResourceBase ||--o{ Instance : "has"
|
|
21
|
+
ResourceBase ||--o{ Work : "has"
|
|
22
|
+
ResourceBase ||--o{ OtherResource : "has"
|
|
23
|
+
ResourceBase ||--o{ ResourceBibframeClass : "has classes"
|
|
24
|
+
ResourceBase ||--o{ Version : "has versions"
|
|
25
|
+
ResourceBase ||--o{ BibframeOtherResources : "has other resources"
|
|
26
|
+
|
|
27
|
+
Work ||--o{ Instance : "has"
|
|
28
|
+
|
|
29
|
+
BibframeClass ||--o{ ResourceBibframeClass : "classifies"
|
|
30
|
+
|
|
31
|
+
OtherResource ||--o{ BibframeOtherResources : "links to"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Database Migrations with Alembic
|
|
35
|
+
The [Alembic](https://alembic.sqlalchemy.org/en/latest/) database migration package is used
|
|
36
|
+
to manage database changes with the Bluecore Data models.
|
|
37
|
+
|
|
38
|
+
To create a new migration, ensure that the Postgres database is available and then run:
|
|
39
|
+
- `uv run alembic revision --autogenerate -m "{short message describing change}`
|
|
40
|
+
|
|
41
|
+
A new migration script will be created in the `bluecore_store_migration` directory. Be sure
|
|
42
|
+
to add the new script to the repository with `git`.
|
|
43
|
+
|
|
44
|
+
#### Applying Migrations
|
|
45
|
+
To apply all of the migrations, run the following command:
|
|
46
|
+
- `uv run alembic upgrade head`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bluecore-models"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Blue Core BIBFRAME Data Models"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"alembic>=1.14.1",
|
|
9
|
+
"psycopg2-binary>=2.9.10",
|
|
10
|
+
"rdflib>=7.1.3",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = [
|
|
15
|
+
"pytest>=8.3.4",
|
|
16
|
+
"pytest-mock-resources>=2.12.1",
|
|
17
|
+
"ruff>=0.9.6",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
license-files = []
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
from bluecore.models.base import Base
|
|
3
|
+
from bluecore.models.resource import ResourceBase
|
|
4
|
+
from bluecore.models.bf_classes import BibframeClass, ResourceBibframeClass
|
|
5
|
+
from bluecore.models.instance import Instance
|
|
6
|
+
from bluecore.models.version import Version
|
|
7
|
+
from bluecore.models.work import Work
|
|
8
|
+
from bluecore.models.other_resource import OtherResource, BibframeOtherResources
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import (
|
|
5
|
+
DateTime,
|
|
6
|
+
ForeignKey,
|
|
7
|
+
Integer,
|
|
8
|
+
String,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from sqlalchemy.orm import (
|
|
12
|
+
mapped_column,
|
|
13
|
+
Mapped,
|
|
14
|
+
relationship,
|
|
15
|
+
)
|
|
16
|
+
from bluecore.models.base import Base
|
|
17
|
+
from bluecore.models.resource import ResourceBase
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BibframeClass(Base):
|
|
21
|
+
__tablename__ = "bibframe_classes"
|
|
22
|
+
|
|
23
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
24
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
25
|
+
uri: Mapped[str] = mapped_column(String, nullable=False, unique=True)
|
|
26
|
+
created_at = mapped_column(DateTime, default=datetime.utcnow)
|
|
27
|
+
updated_at = mapped_column(
|
|
28
|
+
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
return f"<BibframeClass {self.name}>"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ResourceBibframeClass(Base):
|
|
36
|
+
__tablename__ = "resource_bibframe_classes"
|
|
37
|
+
|
|
38
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
39
|
+
bf_class_id: Mapped[int] = mapped_column(
|
|
40
|
+
Integer, ForeignKey("bibframe_classes.id"), nullable=False
|
|
41
|
+
)
|
|
42
|
+
bf_class: Mapped[BibframeClass] = relationship("BibframeClass")
|
|
43
|
+
resource_id: Mapped[int] = mapped_column(
|
|
44
|
+
Integer, ForeignKey("resource_base.id"), nullable=False
|
|
45
|
+
)
|
|
46
|
+
resource: Mapped[ResourceBase] = relationship("ResourceBase", backref="classes")
|
|
47
|
+
|
|
48
|
+
def __repr__(self):
|
|
49
|
+
return f"<ResourceBibframeClass {self.bf_class.name} for {self.resource.uri}>"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from sqlalchemy import (
|
|
2
|
+
ForeignKey,
|
|
3
|
+
Integer,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.orm import (
|
|
7
|
+
mapped_column,
|
|
8
|
+
Mapped,
|
|
9
|
+
relationship,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from bluecore.models.resource import ResourceBase
|
|
13
|
+
|
|
14
|
+
class Instance(ResourceBase):
|
|
15
|
+
__tablename__ = "instances"
|
|
16
|
+
|
|
17
|
+
id: Mapped[int] = mapped_column(
|
|
18
|
+
Integer, ForeignKey("resource_base.id"), primary_key=True
|
|
19
|
+
)
|
|
20
|
+
work_id: Mapped[int] = mapped_column(Integer, ForeignKey("works.id"), nullable=True)
|
|
21
|
+
work: Mapped["Work"] = relationship(
|
|
22
|
+
"Work", foreign_keys=work_id, backref="instances"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__mapper_args__ = {
|
|
26
|
+
"polymorphic_identity": "instances",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return f"<Instance {self.uri}>"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import (
|
|
4
|
+
Boolean,
|
|
5
|
+
DateTime,
|
|
6
|
+
Integer,
|
|
7
|
+
ForeignKey
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from sqlalchemy.orm import (
|
|
11
|
+
mapped_column,
|
|
12
|
+
Mapped,
|
|
13
|
+
relationship,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from bluecore.models.base import Base
|
|
17
|
+
from bluecore.models.resource import ResourceBase
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OtherResource(ResourceBase):
|
|
21
|
+
"""
|
|
22
|
+
Stores JSON or JSON-LD resources used to support Work and Instances.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__tablename__ = "other_resources"
|
|
26
|
+
id: Mapped[int] = mapped_column(
|
|
27
|
+
Integer, ForeignKey("resource_base.id"), primary_key=True
|
|
28
|
+
)
|
|
29
|
+
is_profile: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
30
|
+
|
|
31
|
+
__mapper_args__ = {
|
|
32
|
+
"polymorphic_identity": "other_resources",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
return f"<OtherResource {self.uri or self.id}>"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BibframeOtherResources(Base):
|
|
40
|
+
"""
|
|
41
|
+
Creates relationships between Work or Instance and supporting resources
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
__tablename__ = "bibframe_other_resources"
|
|
45
|
+
|
|
46
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
47
|
+
other_resource_id: Mapped[int] = mapped_column(
|
|
48
|
+
Integer, ForeignKey("other_resources.id"), nullable=False
|
|
49
|
+
)
|
|
50
|
+
other_resource: Mapped[OtherResource] = relationship(
|
|
51
|
+
"OtherResource", foreign_keys=other_resource_id
|
|
52
|
+
)
|
|
53
|
+
bibframe_resource_id: Mapped[int] = mapped_column(
|
|
54
|
+
Integer, ForeignKey("resource_base.id"), nullable=False
|
|
55
|
+
)
|
|
56
|
+
bibframe_resource: Mapped[ResourceBase] = relationship(
|
|
57
|
+
"ResourceBase", backref="other_resources"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
created_at = mapped_column(DateTime, default=datetime.utcnow)
|
|
61
|
+
updated_at = mapped_column(
|
|
62
|
+
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __repr__(self):
|
|
66
|
+
return f"<BibframeOtherResources {self.other_resource.uri or self.other_resource.id} for {self.bibframe_resource.uri}>"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import (
|
|
4
|
+
DateTime,
|
|
5
|
+
String,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from sqlalchemy.orm import (
|
|
9
|
+
mapped_column,
|
|
10
|
+
Mapped,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
14
|
+
|
|
15
|
+
from bluecore.models.base import Base
|
|
16
|
+
|
|
17
|
+
class ResourceBase(Base):
|
|
18
|
+
__tablename__ = "resource_base"
|
|
19
|
+
|
|
20
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
21
|
+
type: Mapped[str] = mapped_column(String, nullable=False)
|
|
22
|
+
data: Mapped[bytes] = mapped_column(JSONB, nullable=False)
|
|
23
|
+
uri: Mapped[str] = mapped_column(String, nullable=True, unique=True)
|
|
24
|
+
created_at = mapped_column(DateTime, default=datetime.utcnow)
|
|
25
|
+
updated_at = mapped_column(
|
|
26
|
+
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__mapper_args__ = {
|
|
30
|
+
"polymorphic_on": type,
|
|
31
|
+
"polymorphic_identity": "resource_base",
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import (
|
|
4
|
+
DateTime,
|
|
5
|
+
Integer,
|
|
6
|
+
ForeignKey
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from sqlalchemy.orm import (
|
|
10
|
+
mapped_column,
|
|
11
|
+
Mapped,
|
|
12
|
+
relationship,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
16
|
+
|
|
17
|
+
from bluecore.models.base import Base
|
|
18
|
+
from bluecore.models.resource import ResourceBase
|
|
19
|
+
|
|
20
|
+
class Version(Base):
|
|
21
|
+
__tablename__ = "versions"
|
|
22
|
+
|
|
23
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
24
|
+
resource_id: Mapped[int] = mapped_column(
|
|
25
|
+
Integer, ForeignKey("resource_base.id"), nullable=False
|
|
26
|
+
)
|
|
27
|
+
resource: Mapped[ResourceBase] = relationship("ResourceBase", backref="versions")
|
|
28
|
+
data: Mapped[bytes] = mapped_column(JSONB, nullable=False)
|
|
29
|
+
created_at = mapped_column(DateTime, default=datetime.utcnow)
|
|
30
|
+
updated_at = mapped_column(
|
|
31
|
+
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return f"<Version at {self.created_at} for {self.resource.uri}>"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from sqlalchemy import (
|
|
2
|
+
ForeignKey,
|
|
3
|
+
Integer,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.orm import (
|
|
7
|
+
mapped_column,
|
|
8
|
+
Mapped,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from bluecore.models.resource import ResourceBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Work(ResourceBase):
|
|
15
|
+
__tablename__ = "works"
|
|
16
|
+
id: Mapped[int] = mapped_column(
|
|
17
|
+
Integer, ForeignKey("resource_base.id"), primary_key=True
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__mapper_args__ = {
|
|
21
|
+
"polymorphic_identity": "works",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
return f"<Work {self.uri}>"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Utility functions for working with RDF graphs."""
|
|
2
|
+
import rdflib
|
|
3
|
+
|
|
4
|
+
BF = rdflib.Namespace("http://id.loc.gov/ontologies/bibframe/")
|
|
5
|
+
BFLC = rdflib.Namespace("http://id.loc.gov/ontologies/bflc/")
|
|
6
|
+
LCLOCAL = rdflib.Namespace("http://id.loc.gov/ontologies/lclocal/")
|
|
7
|
+
MADS = rdflib.Namespace("http://www.loc.gov/mads/rdf/v1#")
|
|
8
|
+
|
|
9
|
+
def init_graph() -> rdflib.Graph:
|
|
10
|
+
"""Initialize a new RDF graph with the necessary namespaces."""
|
|
11
|
+
new_graph = rdflib.Graph()
|
|
12
|
+
new_graph.namespace_manager.bind("bf", BF)
|
|
13
|
+
new_graph.namespace_manager.bind("bflc", BFLC)
|
|
14
|
+
new_graph.namespace_manager.bind("mads", MADS)
|
|
15
|
+
new_graph.namespace_manager.bind("lclocal", LCLOCAL)
|
|
16
|
+
return new_graph
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check_for_namespace(node: rdflib.URIRef) -> bool:
|
|
20
|
+
"""Check if a node is in the LCLOCAL or DCTERMS namespace."""
|
|
21
|
+
return node in LCLOCAL or node in rdflib.DCTERMS
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _exclude_uri_from_other_resources(uri: rdflib.URIRef) -> bool:
|
|
25
|
+
"""Checks if uri is in the BF, MADS, or RDF namespaces"""
|
|
26
|
+
return (
|
|
27
|
+
uri in BF or uri in MADS or uri in rdflib.RDF
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _expand_bnode(graph: rdflib.Graph, entity_graph: rdflib.Graph, bnode: rdflib.BNode):
|
|
32
|
+
"""Expand a blank node in the entity graph."""
|
|
33
|
+
for pred, obj in graph.predicate_objects(subject=bnode):
|
|
34
|
+
if _check_for_namespace(pred) or _check_for_namespace(obj):
|
|
35
|
+
continue
|
|
36
|
+
entity_graph.add((bnode, pred, obj))
|
|
37
|
+
if isinstance(obj, rdflib.BNode):
|
|
38
|
+
_expand_bnode(graph, entity_graph, obj)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_work_or_instance(uri: rdflib.URIRef, graph: rdflib.Graph) -> bool:
|
|
42
|
+
"""Checks if uri is a BIBFRAME Work or Instance"""
|
|
43
|
+
for class_ in graph.objects(subject=uri, predicate=rdflib.RDF.type):
|
|
44
|
+
# In the future we may want to include Work and Instances subclasses
|
|
45
|
+
# maybe through inference
|
|
46
|
+
if class_ == BF.Work or class_ == BF.Instance:
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def generate_entity_graph(graph: rdflib.Graph, entity: rdflib.URIRef) -> rdflib.Graph:
|
|
52
|
+
"""Generate an entity graph from a larger RDF graph."""
|
|
53
|
+
entity_graph = init_graph()
|
|
54
|
+
for pred, obj in graph.predicate_objects(subject=entity):
|
|
55
|
+
if _check_for_namespace(pred) or _check_for_namespace(obj):
|
|
56
|
+
continue
|
|
57
|
+
entity_graph.add((entity, pred, obj))
|
|
58
|
+
if isinstance(obj, rdflib.BNode):
|
|
59
|
+
_expand_bnode(graph, entity_graph, obj)
|
|
60
|
+
return entity_graph
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def generate_other_resources(record_graph: rdflib.Graph, entity_graph: rdflib.Graph) -> list:
|
|
64
|
+
"""
|
|
65
|
+
Takes a Record Graph and Entity Graph and returns a list of dictionaries
|
|
66
|
+
where each dict contains the sub-graphs and URIs that referenced in the
|
|
67
|
+
entity graph and present in the record graph.
|
|
68
|
+
"""
|
|
69
|
+
other_resources = []
|
|
70
|
+
for row in entity_graph.query("""
|
|
71
|
+
SELECT DISTINCT ?object
|
|
72
|
+
WHERE {
|
|
73
|
+
?subject ?predicate ?object .
|
|
74
|
+
FILTER(isIRI(?object))
|
|
75
|
+
}
|
|
76
|
+
"""):
|
|
77
|
+
uri = row[0]
|
|
78
|
+
if _exclude_uri_from_other_resources(uri) or _is_work_or_instance(uri, record_graph):
|
|
79
|
+
continue
|
|
80
|
+
other_resource_graph = generate_entity_graph(record_graph, uri)
|
|
81
|
+
if len(other_resource_graph) > 0:
|
|
82
|
+
other_resources.append(
|
|
83
|
+
{
|
|
84
|
+
"uri": str(uri),
|
|
85
|
+
"graph": other_resource_graph
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
return other_resources
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: bluecore-models
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Blue Core BIBFRAME Data Models
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: alembic>=1.14.1
|
|
8
|
+
Requires-Dist: psycopg2-binary>=2.9.10
|
|
9
|
+
Requires-Dist: rdflib>=7.1.3
|
|
10
|
+
|
|
11
|
+
# Blue Core Data Models
|
|
12
|
+
The Blue Core Data Models are used in [Blue Core API](https://github.com/blue-core-lod/bluecore_api)
|
|
13
|
+
and in the [Blue Core Workflows](https://github.com/blue-core-lod/bluecore-workflows) services.
|
|
14
|
+
|
|
15
|
+
## Run Postgres with Docker
|
|
16
|
+
To run the Postgres with the Blue Core Database, run the following command from this directory:
|
|
17
|
+
|
|
18
|
+
`docker run --name bluecore_db -e POSTGRES_USER=bluecore_admin -e POSTGRES_PASSWORD=bluecore_admin -v ./create-db.sql:/docker-entrypoint-initdb.d/create_database.sql -p 5432:5432 postgres:17`
|
|
19
|
+
|
|
20
|
+
## Installing
|
|
21
|
+
- Install via pip: `pip install blue-core-data-models`
|
|
22
|
+
- Install via uv: `uv add blue-core-data-models`
|
|
23
|
+
|
|
24
|
+
## Database Management
|
|
25
|
+
The [SQLAlchemy](https://www.sqlalchemy.org/) Object Relational Mapper (ORM) is used to create
|
|
26
|
+
the Bluecore database models.
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
erDiagram
|
|
30
|
+
ResourceBase ||--o{ Instance : "has"
|
|
31
|
+
ResourceBase ||--o{ Work : "has"
|
|
32
|
+
ResourceBase ||--o{ OtherResource : "has"
|
|
33
|
+
ResourceBase ||--o{ ResourceBibframeClass : "has classes"
|
|
34
|
+
ResourceBase ||--o{ Version : "has versions"
|
|
35
|
+
ResourceBase ||--o{ BibframeOtherResources : "has other resources"
|
|
36
|
+
|
|
37
|
+
Work ||--o{ Instance : "has"
|
|
38
|
+
|
|
39
|
+
BibframeClass ||--o{ ResourceBibframeClass : "classifies"
|
|
40
|
+
|
|
41
|
+
OtherResource ||--o{ BibframeOtherResources : "links to"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Database Migrations with Alembic
|
|
45
|
+
The [Alembic](https://alembic.sqlalchemy.org/en/latest/) database migration package is used
|
|
46
|
+
to manage database changes with the Bluecore Data models.
|
|
47
|
+
|
|
48
|
+
To create a new migration, ensure that the Postgres database is available and then run:
|
|
49
|
+
- `uv run alembic revision --autogenerate -m "{short message describing change}`
|
|
50
|
+
|
|
51
|
+
A new migration script will be created in the `bluecore_store_migration` directory. Be sure
|
|
52
|
+
to add the new script to the repository with `git`.
|
|
53
|
+
|
|
54
|
+
#### Applying Migrations
|
|
55
|
+
To apply all of the migrations, run the following command:
|
|
56
|
+
- `uv run alembic upgrade head`
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/bluecore/models/__init__.py
|
|
4
|
+
src/bluecore/models/base.py
|
|
5
|
+
src/bluecore/models/bf_classes.py
|
|
6
|
+
src/bluecore/models/instance.py
|
|
7
|
+
src/bluecore/models/other_resource.py
|
|
8
|
+
src/bluecore/models/resource.py
|
|
9
|
+
src/bluecore/models/version.py
|
|
10
|
+
src/bluecore/models/work.py
|
|
11
|
+
src/bluecore/utils/graph.py
|
|
12
|
+
src/bluecore_models.egg-info/PKG-INFO
|
|
13
|
+
src/bluecore_models.egg-info/SOURCES.txt
|
|
14
|
+
src/bluecore_models.egg-info/dependency_links.txt
|
|
15
|
+
src/bluecore_models.egg-info/requires.txt
|
|
16
|
+
src/bluecore_models.egg-info/top_level.txt
|
|
17
|
+
tests/test_models.py
|
|
18
|
+
tests/test_utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bluecore
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
from datetime import datetime, UTC
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from pytest_mock_resources import create_sqlite_fixture, Rows
|
|
7
|
+
|
|
8
|
+
from sqlalchemy.orm import sessionmaker
|
|
9
|
+
|
|
10
|
+
from bluecore.models import (
|
|
11
|
+
Base,
|
|
12
|
+
BibframeClass,
|
|
13
|
+
ResourceBibframeClass,
|
|
14
|
+
Instance,
|
|
15
|
+
OtherResource,
|
|
16
|
+
Version,
|
|
17
|
+
Work,
|
|
18
|
+
BibframeOtherResources,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_test_rows():
|
|
23
|
+
return Rows(
|
|
24
|
+
# BibframeClass
|
|
25
|
+
BibframeClass(
|
|
26
|
+
id=1,
|
|
27
|
+
name="Instance",
|
|
28
|
+
uri="http://id.loc.gov/ontologies/bibframe/Instance",
|
|
29
|
+
created_at=datetime.now(UTC),
|
|
30
|
+
updated_at=datetime.now(UTC),
|
|
31
|
+
),
|
|
32
|
+
BibframeClass(
|
|
33
|
+
id=2,
|
|
34
|
+
name="Work",
|
|
35
|
+
uri="http://id.loc.gov/ontologies/bibframe/Work",
|
|
36
|
+
created_at=datetime.now(UTC),
|
|
37
|
+
updated_at=datetime.now(UTC),
|
|
38
|
+
),
|
|
39
|
+
# Work
|
|
40
|
+
Work(
|
|
41
|
+
id=1,
|
|
42
|
+
uri="https://bluecore.info/work/e0d6-40f0-abb3-e9130622eb8a",
|
|
43
|
+
created_at=datetime.now(UTC),
|
|
44
|
+
updated_at=datetime.now(UTC),
|
|
45
|
+
data=pathlib.Path("tests/blue-core-work.jsonld").read_text(),
|
|
46
|
+
type="works",
|
|
47
|
+
),
|
|
48
|
+
# Instance
|
|
49
|
+
Instance(
|
|
50
|
+
id=2,
|
|
51
|
+
uri="https://bluecore.info/instance/75d831b9-e0d6-40f0-abb3-e9130622eb8a",
|
|
52
|
+
created_at=datetime.now(UTC),
|
|
53
|
+
updated_at=datetime.now(UTC),
|
|
54
|
+
data=pathlib.Path("tests/blue-core-instance.jsonld").read_text(),
|
|
55
|
+
type="instances",
|
|
56
|
+
work_id=1,
|
|
57
|
+
),
|
|
58
|
+
# OtherResource
|
|
59
|
+
OtherResource(
|
|
60
|
+
id=3,
|
|
61
|
+
uri="https://bluecore.info/other-resource/sample",
|
|
62
|
+
created_at=datetime.now(UTC),
|
|
63
|
+
updated_at=datetime.now(UTC),
|
|
64
|
+
data='{"description": "Sample Other Resource"}',
|
|
65
|
+
type="other_resources",
|
|
66
|
+
is_profile=False,
|
|
67
|
+
),
|
|
68
|
+
# ResourceBibframeClass
|
|
69
|
+
ResourceBibframeClass(
|
|
70
|
+
id=1,
|
|
71
|
+
bf_class_id=1,
|
|
72
|
+
resource_id=2,
|
|
73
|
+
),
|
|
74
|
+
ResourceBibframeClass(
|
|
75
|
+
id=2,
|
|
76
|
+
bf_class_id=2,
|
|
77
|
+
resource_id=1,
|
|
78
|
+
),
|
|
79
|
+
# Version
|
|
80
|
+
Version(
|
|
81
|
+
id=1,
|
|
82
|
+
resource_id=1,
|
|
83
|
+
data=pathlib.Path("tests/blue-core-work.jsonld").read_text(),
|
|
84
|
+
created_at=datetime.now(UTC),
|
|
85
|
+
updated_at=datetime.now(UTC),
|
|
86
|
+
),
|
|
87
|
+
Version(
|
|
88
|
+
id=2,
|
|
89
|
+
resource_id=2,
|
|
90
|
+
data=pathlib.Path("tests/blue-core-instance.jsonld").read_text(),
|
|
91
|
+
created_at=datetime.now(UTC),
|
|
92
|
+
updated_at=datetime.now(UTC),
|
|
93
|
+
),
|
|
94
|
+
# BibframeOtherResources
|
|
95
|
+
BibframeOtherResources(
|
|
96
|
+
id=1,
|
|
97
|
+
other_resource_id=3,
|
|
98
|
+
bibframe_resource_id=1,
|
|
99
|
+
created_at=datetime.now(UTC),
|
|
100
|
+
updated_at=datetime.now(UTC),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
engine = create_sqlite_fixture(create_test_rows())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.fixture()
|
|
109
|
+
def pg_session(engine):
|
|
110
|
+
Base.metadata.create_all(engine)
|
|
111
|
+
return sessionmaker(bind=engine)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_bibframe_class(pg_session):
|
|
115
|
+
with pg_session() as session:
|
|
116
|
+
bf_instance = (
|
|
117
|
+
session.query(BibframeClass).where(BibframeClass.name == "Instance").first()
|
|
118
|
+
)
|
|
119
|
+
assert bf_instance is not None
|
|
120
|
+
assert bf_instance.uri.startswith(
|
|
121
|
+
"http://id.loc.gov/ontologies/bibframe/Instance"
|
|
122
|
+
)
|
|
123
|
+
assert bf_instance.created_at
|
|
124
|
+
assert bf_instance.updated_at
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_resource_bibframe_class(pg_session):
|
|
128
|
+
with pg_session() as session:
|
|
129
|
+
resource_bf_class = (
|
|
130
|
+
session.query(ResourceBibframeClass)
|
|
131
|
+
.where(ResourceBibframeClass.id == 1)
|
|
132
|
+
.first()
|
|
133
|
+
)
|
|
134
|
+
assert resource_bf_class.resource.uri.startswith(
|
|
135
|
+
"https://bluecore.info/instance"
|
|
136
|
+
)
|
|
137
|
+
assert resource_bf_class.bf_class.name == "Instance"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_instance(pg_session):
|
|
141
|
+
with pg_session() as session:
|
|
142
|
+
instance = session.query(Instance).where(Instance.id == 2).first()
|
|
143
|
+
assert instance.uri.startswith("https://bluecore.info/instance")
|
|
144
|
+
assert instance.data
|
|
145
|
+
assert instance.created_at
|
|
146
|
+
assert instance.updated_at
|
|
147
|
+
assert instance.work is not None
|
|
148
|
+
assert instance.work.uri.startswith("https://bluecore.info/work")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_work(pg_session):
|
|
152
|
+
with pg_session() as session:
|
|
153
|
+
work = session.query(Work).where(Work.id == 1).first()
|
|
154
|
+
assert work.uri.startswith("https://bluecore.info/work")
|
|
155
|
+
assert work.data
|
|
156
|
+
assert work.created_at
|
|
157
|
+
assert work.updated_at
|
|
158
|
+
assert len(work.instances) > 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_other_resource(pg_session):
|
|
162
|
+
with pg_session() as session:
|
|
163
|
+
other_resource = (
|
|
164
|
+
session.query(OtherResource).where(OtherResource.id == 3).first()
|
|
165
|
+
)
|
|
166
|
+
assert other_resource.uri.startswith("https://bluecore.info/other-resource")
|
|
167
|
+
assert other_resource.data
|
|
168
|
+
assert other_resource.created_at
|
|
169
|
+
assert other_resource.updated_at
|
|
170
|
+
assert other_resource.is_profile is False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_versions(pg_session):
|
|
174
|
+
with pg_session() as session:
|
|
175
|
+
version = session.query(Version).where(Version.id == 1).first()
|
|
176
|
+
work = session.query(Work).where(Work.id == 1).first()
|
|
177
|
+
assert version.resource is not None
|
|
178
|
+
assert version.resource == work
|
|
179
|
+
assert version.data
|
|
180
|
+
assert version.created_at
|
|
181
|
+
assert version.updated_at
|
|
182
|
+
version2 = session.query(Version).where(Version.id == 2).first()
|
|
183
|
+
instance = session.query(Instance).where(Instance.id == 2).first()
|
|
184
|
+
assert version2.resource == instance
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_bibframe_other_resources(pg_session):
|
|
188
|
+
with pg_session() as session:
|
|
189
|
+
bibframe_other_resource = (
|
|
190
|
+
session.query(BibframeOtherResources)
|
|
191
|
+
.where(BibframeOtherResources.id == 1)
|
|
192
|
+
.first()
|
|
193
|
+
)
|
|
194
|
+
assert bibframe_other_resource.other_resource is not None
|
|
195
|
+
assert bibframe_other_resource.bibframe_resource is not None
|
|
196
|
+
assert bibframe_other_resource.created_at
|
|
197
|
+
assert bibframe_other_resource.updated_at
|
|
198
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import rdflib
|
|
4
|
+
|
|
5
|
+
from bluecore.utils.graph import (
|
|
6
|
+
BF,
|
|
7
|
+
BFLC,
|
|
8
|
+
MADS,
|
|
9
|
+
generate_entity_graph,
|
|
10
|
+
init_graph,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
def test_init_graph():
|
|
14
|
+
graph = init_graph()
|
|
15
|
+
assert graph.namespace_manager.store.namespace("bf") == rdflib.URIRef(BF)
|
|
16
|
+
assert graph.namespace_manager.store.namespace("bflc") == rdflib.URIRef(BFLC)
|
|
17
|
+
assert graph.namespace_manager.store.namespace("mads") == rdflib.URIRef(MADS)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_generate_entity_graph():
|
|
21
|
+
loc_graph = init_graph()
|
|
22
|
+
loc_graph.parse(data=pathlib.Path("tests/23807141.jsonld").read_text(), format="json-ld")
|
|
23
|
+
work_uri = rdflib.URIRef("http://id.loc.gov/resources/works/23807141")
|
|
24
|
+
dcterm_part_of = loc_graph.value(subject=work_uri, predicate=rdflib.DCTERMS.isPartOf)
|
|
25
|
+
assert dcterm_part_of == rdflib.URIRef("http://id.loc.gov/resources/works")
|
|
26
|
+
work_graph = generate_entity_graph(loc_graph, work_uri)
|
|
27
|
+
assert len(work_graph) == 118
|
|
28
|
+
|
|
29
|
+
work_title = work_graph.value(subject=work_uri, predicate=BF.title)
|
|
30
|
+
main_title = work_graph.value(subject=work_title, predicate=BF.mainTitle)
|
|
31
|
+
assert str(main_title).startswith("HBR guide to generative AI for managers")
|
|
32
|
+
|
|
33
|
+
# Tests if DCTERMs triples are filtered out of entity graph
|
|
34
|
+
work_dcterm_part_of = work_graph.value(subject=work_uri, predicate=rdflib.DCTERMS.isPartOf)
|
|
35
|
+
assert work_dcterm_part_of is None
|
|
36
|
+
|