Deepsel CMS Backend - FastAPI application for content management
Project description
Deepsel Python Template
Set up for local development
Python environment
Python >= 3.12 is required. You may choose to set up a python virtual environment before installing dependencies.
pip install -e .
Run a Postgres database
# we need an env file
cp .env.sample .env
# use pre-made definition
docker-compose -f deepsel-local-docker-compose.yml up -d
Install Node dependencies (for JSX compilation)
React-based templates are compiled server-side using esbuild.
Install the Node dependencies once in this directory:
npm install
Run
uvicorn main:app --reload
Environment Variables for Migration Control
The application supports environment variables to control database migration behavior, particularly useful for Kubernetes deployments:
LOCAL_PACKAGES
- Purpose: Build
@deepselpackages from local source instead of npm - Usage:
LOCAL_PACKAGES=true - Behavior: When true, builds
@deepsel/cms-utilsand@deepsel/cms-reactfrom the localpackages/directory instead of pulling from npm
NO_CLIENT
- Purpose: Skip starting the Astro client during backend startup
- Usage:
NO_CLIENT=true - Behavior: Useful for local development when running the client separately via
cd client && npm run dev
CLIENT_HOST
- Purpose: Set the host for the Astro client server
- Default:
0.0.0.0
CLIENT_PORT
- Purpose: Set the port for the Astro client server
- Default:
4321
ONLY_MIGRATE
- Purpose: Run database migrations only and exit (for init containers)
- Usage: ONLY_MIGRATE=true env set
- Behavior:
- Runs database schema migrations
- Installs apps and imports CSV data
- Runs startup logic
- Exits with code 0 on success
NO_MIGRATE
- Purpose: Skip database migrations and startup logic (for main containers)
- Usage: NO_MIGRATE=true env set
- Behavior:
- Skips database migrations
- Skips startup logic execution
- Starts the web server normally
Auto-format code
Auto code formatting is done by the black package, included in dev dependencies.
Auto-format the code with:
make format
Code checking before commit
Code checking is required before committing the code, and the CI/CD pipeline will also check the code using the same steps before deploying.
The code checking is done by the following tools:
blackfor code formattingflake8for lintingbanditfor security checkspytestfor unit tests
To setup the tools, install with dev dependencies:
pip install -e ".[dev]"
Before committing the code, you should check the code with the following commands:
make lint security test
Note that a database is required to run the tests. You can use the same database as the development database.
A typical app structure
All apps are stored in the apps directory. The deepsel directory is a special app that contains the base models, routes, and services that are shared across all apps.
This folder is kept in the top-level directory to make it easier to share code between apps.
It maybe renamed to core in white-label projects.
To create a new app, for example an e-commerce backend ,you can use create a new folder ecommerce in the apps directory and create the following structure:
apps/
ecommerce/
__init__.py
data/
__init__.py
order.csv
order_item.csv
product.csv
models/
__init__.py
order.py
order_item.py
product.py
routers/
__init__.py
order.py
order_item.py
product.py
templates/
order_confirm_email.html
utils/
__init__.py
my_function.py
High-level overview of the app structure
datacontains the data files that are used to seed the database. These are CSV files, and theimport_order(e.g. we must create products before we can create orders) is defined in thedata/__init__.pyfile.modelscontains the SQLAlchemy database models that are used to interact with the database. The folder name must bemodelsfor the auto DB manager to work.routerscontains the FastAPI routers that define the API endpoints. The folder name must beroutersfor the auto router installer to work.templatescontains any text data that are too large to be stored in the codebase or the data CSV files. For example, email templates. The folder name is a convention and not essential to the core structure.utilscontains any utility functions that you wish to use and share. The folder name is a convention and not essential to the core structure.
Naming conventions
Table names
Lowercase and snake_case. For example, order_item.
Not pluralized. For example, order_item instead of order_items.
Model names
CamelCaseModel. For example, OrderItemModel, ProductModel.
Router names
Every router should be assigned to a variable named router. This is required for the auto router installer to work, it will look for a variable named router in the file.
File names
Every model or router file should be the same as the table names. For example, models/product.py and routers/product.py.
For CSV data files, the file name is required to be the table name, for the app installer to insert the records into the correct table.
For example, data/product.csv.
How to create a model
To create a model, you need to create a new file in the models directory.
For example, to create a product model, you need to create a file named product.py in the models directory.
The file should contain the following code:
from sqlalchemy import Column, Integer, String, Text, Float
from db import Base
from deepsel.mixins.base_model import BaseModel
class ProductModel(Base, BaseModel):
__tablename__ = "product"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False) # Strings will be rendered as TextInput by the JSX generator
price = Column(Float) # Floats will be rendered as NumberInput, NumberFormatter by the JSX generator
description = Column(Text) # Text will be rendered as TextArea by the JSX generator
The Base is the standard SQLAlchemy declarative base.
The BaseModel is a mixin defined in the deepsel app that adds the following fields to the model:
created_at(DateTime)updated_at(DateTime)string_id(String)system(Boolean)owner_id(ForeignKey to User)organization_id(ForeignKey to Organization)
It also includes CRUD methods that are shared across all models.
These methods ensure permissions are checked before creating, reading, updating, or deleting records.
They will also manage record ownership and organization data separation by using owner_id and organization_id.
These methods are:
createget_oneget_allsearchupdatedeletebulk_delete
You can find out how these methods work in deepsel/mixins/orm.py.
How to create a router
To create a router, you need to create a new file in the routers directory.
For example, to create a product router, you need to create a file named product.py in the routers directory.
The file should contain the following code:
from deepsel.utils.crud_router import CRUDRouter
from deepsel.utils.generate_crud_schemas import generate_CRUD_schemas
from deepsel.utils.get_current_user import get_current_user
from fastapi import Depends
table_name = "product"
CRUDSchemas = generate_CRUD_schemas(table_name)
router = CRUDRouter(
read_schema=CRUDSchemas.Read,
search_schema=CRUDSchemas.Search,
create_schema=CRUDSchemas.Create,
update_schema=CRUDSchemas.Update,
table_name=table_name,
dependencies=[Depends(get_current_user)],
)
The schemas are Pydantic models that are generated automatically, and will define the fields that are returned by the API endpoints (and also build the Swagger documentation).
How to customize a router
You can customize the router by adding additional endpoints or modifying the existing ones.
For example, to add a custom endpoint to the product router, you can add the following code to the product.py file:
from deepsel.utils.crud_router import CRUDRouter
from deepsel.utils.generate_crud_schemas import generate_CRUD_schemas
from deepsel.utils.get_current_user import get_current_user
from fastapi import Depends
table_name = "product"
CRUDSchemas = generate_CRUD_schemas(table_name)
router = CRUDRouter(
read_schema=CRUDSchemas.Read,
search_schema=CRUDSchemas.Search,
create_schema=CRUDSchemas.Create,
update_schema=CRUDSchemas.Update,
table_name=table_name,
dependencies=[Depends(get_current_user)]
)
@router.get("/custom_endpoint")
def custom_endpoint():
return {"message": "This is a custom endpoint"}
Or if you want to modify the existing endpoint, you can override that route builder method in the router class. For example, overriding the search route to only allow search to return products with a price greater than 100:
from deepsel.utils.crud_router import CRUDRouter
from deepsel.utils.generate_crud_schemas import generate_CRUD_schemas
from deepsel.utils.get_current_user import get_current_user
from fastapi import Depends
from typing import Any, Optional
from deepsel.mixins.orm import PermissionAction, PermissionScope, SearchCriteria, Operator
from deepsel.utils.crud_router import CRUDRouter, CALLABLE_DICT, PAGINATION, SearchQuery, OrderByCriteria
from sqlalchemy.orm import Session
table_name = "product"
CRUDSchemas = generate_CRUD_schemas(table_name)
class ProductCustomRouter(CRUDRouter):
def _search(self, *args: Any, **kwargs: Any) -> CALLABLE_DICT:
def route(
db: Session = Depends(self.db_func),
user: UserModel = Depends(get_current_user),
pagination: PAGINATION = self.pagination,
search: Optional[SearchQuery] = None,
order_by: Optional[OrderByCriteria] = None,
) -> dict:
custom_criteria = SearchCriteria(
field="price",
operator=Operator.gt,
value=100
)
search.AND.append(custom_criteria)
return super()._search(db, user, pagination, search, order_by)
return route
router = ProductCustomRouter(
read_schema=CRUDSchemas.Read,
search_schema=CRUDSchemas.Search,
create_schema=CRUDSchemas.Create,
update_schema=CRUDSchemas.Update,
table_name=table_name,
dependencies=[Depends(get_current_user)]
)
How to customize an endpoint schema
You can customize the schema by adding additional fields or modifying the existing ones.
For example, to add a custom field called custom_field to the product get one schema, you can add the following code to the product.py file:
from apps.ecommerce.models.product import ProductModel as Model
from deepsel.models.user import UserModel
from deepsel.utils.generate_crud_schemas import generate_CRUD_schemas
from deepsel.utils.get_current_user import get_current_user
from fastapi import Depends
from typing import Any
from deepsel.utils.crud_router import CRUDRouter, CALLABLE
from sqlalchemy.orm import Session
table_name = "product"
CRUDSchemas = generate_CRUD_schemas(table_name)
class CustomReadSchema(CRUDSchemas.Read):
custom_field: str
class ProductCustomRouter(CRUDRouter):
def _get_one(self, *args: Any, **kwargs: Any) -> CALLABLE:
def route(
item_id: self._pk_type,
db: Session = Depends(self.db_func),
user: UserModel = Depends(get_current_user),
) -> Model:
item = super()._get_one(item_id, db, user)
item.custom_field = "This is a custom field value"
return item
return route
router = ProductCustomRouter(
read_schema=CustomReadSchema,
search_schema=CRUDSchemas.Search,
create_schema=CRUDSchemas.Create,
update_schema=CRUDSchemas.Update,
table_name=table_name,
dependencies=[Depends(get_current_user)]
)
How to customize a model
The same result of the above example can be achieved by adding a customizing the search method of the model instead of the router:
from sqlalchemy import Column, Integer, String, Text, Float
from db import Base
from deepsel.mixins.base_model import BaseModel
from deepsel.mixins.orm import SearchCriteria, Operator
from deepsel.utils.crud_router import PAGINATION, SearchQuery, OrderByCriteria
from sqlalchemy.orm import Session
from typing import Any, Optional
class ProductModel(Base, BaseModel):
#... model definition
@classmethod
def search(
cls,
db: Session,
user: "UserModel",
pagination: PAGINATION,
search: Optional[SearchQuery] = None,
order_by: Optional[OrderByCriteria] = None,
*args,
**kwargs,
):
if search is None:
search = SearchQuery()
custom_criteria = SearchCriteria(
field="price",
operator=Operator.gt,
value=100
)
search.AND.append(custom_criteria)
return super().search(db, user, pagination, search, order_by)
How the auto schema generator works
Read schemas
The read schemas will automatically include all defined fields of the database model.
If a relationship is defined in the model, the schema will expand the related record(s) and include all of the related fields, recursively.
Example:
from sqlalchemy import Column, Integer, String, Text, Float, ForeignKey
from sqlalchemy.orm import relationship
from db import Base
from deepsel.mixins.base_model import BaseModel
class ProductModel(Base, BaseModel):
__tablename__ = "product"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
category_id = Column(Integer, ForeignKey('category.id'))
category = relationship("CategoryModel")
class CategoryModel(Base, BaseModel):
__tablename__ = "category"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
category_type_id = Column(Integer, ForeignKey('category_type.id'))
type = relationship("CategoryTypeModel")
class CategoryTypeModel(Base, BaseModel):
__tablename__ = "category_type"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
This will be the resulting read schema for ProductModel:
{
"id": 0,
"name": "string",
"category_id": 0,
"category": {
"id": 0,
"name": "string",
"category_type_id": 0,
"type": {
"id": 0,
"name": "string"
}
}
}
As all relationships are included in the read schemas, it is important to be mindful of the depth of the relationships, as this can lead to very large schemas and slow API responses.
You may choose to exclude certain relationships by customizing the schemas as above.
Create and Update schemas
The create and update schemas will include all fields of the database model that are not id or relationships.
The BaseModel mixin fields such as created_at, updated_at, string_id, system, owner_id, and organization_id are also excluded, as they are assigned automatically.
Fields will be marked as required in the schemas if they are defined as nullable=False in the model. Not providing a required field will result in a validation error (HTTP 422).
How to define a Many2One relationship
A Many2One relationship is where a record links to another record.
For example, a product record may link to a category record.
To define a Many2One relationship, you need to define a ForeignKey field in the model.
For example, to define a Many2One relationship between product and category, you need to add the following field to the product model:
from sqlalchemy import Column, Integer, String, Text, Float, ForeignKey
from sqlalchemy.orm import relationship
from db import Base
from deepsel.mixins.base_model import BaseModel
class ProductModel(Base, BaseModel):
__tablename__ = "product"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
category_id = Column(Integer, ForeignKey('category.id'))
category = relationship("CategoryModel")
This will be the resulting read schema for ProductModel:
{
"id": 0,
"name": "string",
"category_id": 0,
"category": {
"id": 0,
"name": "string"
}
}
How to define a One2Many relationship
A One2Many relationship is where a record links to multiple other records in a parent-child manner.
The children will have a ForeignKey field that links back to the parent record.
This way we don't need to define third table to store the relationship like in Many2Many relationships.
An example use case is a order record (parent) that links to multiple order_item records (children).
To define a One2Many relationship, you need to define a ForeignKey field in the child model.
For example, to define a One2Many relationship between order and order_item, you need to add the following field to the order_item model:
from sqlalchemy import Column, Integer, String, Text, Float, ForeignKey
from sqlalchemy.orm import relationship
from db import Base
from deepsel.mixins.base_model import BaseModel
class OrderModel(Base, BaseModel):
__tablename__ = "order"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
items = relationship("OrderItemModel")
class OrderItemModel(Base, BaseModel):
__tablename__ = "order_item"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
order_id = Column(Integer, ForeignKey('order.id'))
This will be the resulting read schema for OrderModel:
{
"id": 0,
"name": "string",
"items": [
{
"id": 0,
"order_id": 0,
"name": "string"
}
]
}
How to define a Many2Many relationship
A Many2Many relationship is where a record links to multiple other records in a peer-to-peer manner.
An example use case is a product record that links to multiple tag records, and these tag records can be linked to multiple product records.
To define a Many2Many relationship, you need to define a third model that links the two models together. This is called an association/junction table.
For example, to define a Many2Many relationship between product and tag, you need to add the following fields to the product and tag models:
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from db import Base
from deepsel.mixins.base_model import BaseModel
from deepsel.mixins.orm import ORMBaseMixin
class ProductModel(Base, BaseModel):
__tablename__ = "product"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
tags = relationship("TagModel", secondary="product_tag")
class TagModel(Base, BaseModel):
__tablename__ = "tag"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
products = relationship("ProductModel", secondary="product_tag")
class ProductTagModel(Base, ORMBaseMixin): # Pls note to use ORMBaseMixin instead of BaseModel, we don't need organization/ownership control fields
__tablename__ = "product_tag"
product_id = Column(Integer, ForeignKey('product.id'), nullable=False, primary_key=True)
tag_id = Column(Integer, ForeignKey('tag.id'), nullable=False, primary_key=True)
This will be the resulting read schema for ProductModel:
{
"id": 0,
"name": "string",
"tags": [
{
"id": 0,
"name": "string"
}
]
}
And this will be the resulting read schema for TagModel:
{
"id": 0,
"name": "string",
"products": [
{
"id": 0,
"name": "string"
}
]
}
How to create a router for a model with auto-generated CRUD Endpoints
This guide will help you to create a router for a model with automatically generated CRUD (Create, Read, Update, Delete) endpoints using FastAPI library.
Step 1: Import Necessary Modules
First, you need to import the necessary modules from FastAPI and your application. Here's what you need:
from fastapi import Depends
from deepsel.utils.crud_router import CRUDRouter
from deepsel.utils.generate_crud_schemas import generate_CRUD_schemas
from deepsel.utils.get_current_user import get_current_user
Step 2: Import Your Model
Next, you need to import the model you want to create a router for. Replace YourModel with the model you're working with:
from your_app.models import YourModel as Model
Step 3: Generate CRUD Schemas
You can generate Pydantic models for the CRUD operations using generate_CRUD_schemas. These models define the fields that are returned by the API endpoints:
[ReadSchema, CreateSchema, UpdateSchema, SearchSchema] = generate_CRUD_schemas(Model)
Step 4: Create the Router
Now, you can create the router using CRUDRouter. Pass the schemas you just created, your model, and any dependencies your router has:
router = CRUDRouter(
read_schema=ReadSchema,
search_schema=SearchSchema,
create_schema=CreateSchema,
update_schema=UpdateSchema,
db_model=Model,
dependencies=[Depends(get_current_user)]
)
Optional: Customize the Router
You can customize the router by adding additional endpoints or modifying the existing ones. For example, to add a custom endpoint to your router, you can add the following code:
@router.get("/custom_endpoint")
async def custom_endpoint():
pass
Optional: Read-Only Models
For read-only models, you can disable the update, delete, bulk_delete and create routes by setting the corresponding parameters to False:
router = CRUDRouter(
read_schema=ReadSchema,
search_schema=SearchSchema,
db_model=Model,
dependencies=[Depends(get_current_user)],
update_route=False,
delete_one_route=False,
bulk_delete_route=False,
create_route=False
)
By following these steps, you can easily create a router with autogenerated CRUD endpoints for your models.
How does the seeding data (CSV file) work
When the source is starting, the install_apps will be executed to save the seeding data (csv file) into the database. You can add more data automatically with multiple types, correctly related data, and keep previous data.
The data (csv file):
-
These files must be in
.csvformat and stored in thedatadirectory of each app, or in the data directory of Deepsel (in case of a bulk deployment).- App directory:
./apps/{app_name}/data/ - Deepsel directory:
./deepsel/data/
- App directory:
-
The file name need to be the same as the model name (database table name).
-
File names need to be included for
import_orderin the__init__.pyfiles for each data directory. The position of the file name in theimport_orderlist is also the launch priority.
Column names (in CSV file):
- They should match the column names of the model (table column).
- Special cases for column naming will be described below.
Special Common Names (in CSV file):
string_id
- Holds a unique value to identify the data on database that is from the CSV file.
- This field is required.
- It will be stored in the
string_idcolumn (database table). - If the database table already has a
string_id, the row will be skipped.
{related_table_name}/{refecenced_column}
- You have to provide
string_id(temporary callfile_value) of the record at therelated_tablethat you want to relate - The saved value of this column (database) will be the
id(primary id) ofrelated_tablerecord, provided thatrelated_table.string_id === file_value.
file:{column_name}
- You have to provide the path of file (starting from the root directory). For example:
deepsel/path/to/your/file. - The saved value of this column will be the content of that file (large data).
json:{column_name}
- You have to provide the stringify of JSON
- The saved value of this column will be the JSON value.
owner_id
- It may be
user/owner_idcolumn, witch is in the format{related_table_name}/{refecenced_column}as explained above. - If the CSV file does not contain a
user/owner_idorowner_id column, by default it is supposed to have auser/owner_idcolumn with the value that issuper_user. - If the CSV file contains a
user/owner_idorowner_idcolumn, the corresponding value will be stored.
organization_id
- It may be
organization/organization_idcolumn, witch be in the format{related_table_name}/{refecenced_column}as explained above. - If the CSV file does not contain an
organization/organization_idcolumn ororganization_id, by default it is supposed to have aorganization/organization_idcolumn with the value that isDEFAULT_ORG_ID(.../constants/init.py). - If the CSV file contains an
organization/organization_idororganization_idcolumn, the corresponding ID value will be stored.
Example for Seeding data - CSV file
We have a database table product with the structure and previous data like this:
| id | name | template | meta | category_id | owner_id | organization_id | string_id |
|---|---|---|---|---|---|---|---|
| 1 | product 1 | the template file data... |
{ color: 'red' } | 3 | 2 | 1 | product_1 |
We have to create the seeding data (csv file) that will be added into product table:
- File path/name:
app/product/data/product.csv - Declare the file name into file
app/product/__init__.py, with the order of priority you desire. - File data:
| string_id | name | file:template | meta | category/category_id | user/owner_id | |
|---|---|---|---|---|---|---|
| product_1 | product | path\to\file1 |
{ color: 'red' } | category_string_id_1 |
user_string_id_1 |
|
| product_2 | product 2 | path\to\file2 |
{ color: 'blue' } | category_string_id_2 |
user_string_id_2 |
Row by row data analysis:
- Row 1: Because the
string_idisproduct_1this already exists inv the product data (previous data in database), so it will automatically be discarded and this row will not be processed. - Row 2: Product table is added a new record with:
- data of columns
string_id,namethese are mapped withstring_id,nameof the csv file - column
template, the data saved is the content from file of the urlpath\to\file2 - column
metathe data saved is JSON{ color: 'blue' } - column
category_id, the data saved is theidof the recodecategorywhosestring_id=category_string_id_2. (Example, the value is:12) - column
owner_id, the data saved is theidof the recodeuserwhosestring_id=user_string_id_2. (Example, the value is:22) - column
organization_id, the data saved is the default id (DEFAULT_ORG_ID). (Example, the value is:1)
- data of columns
Finally, we have the product table data that is:
| id | name | template | meta | category_id | owner_id | organization_id | string_id |
|---|---|---|---|---|---|---|---|
| 1 | product 1 | the template file data... |
{ color: 'red' } | 3 | 2 | 1 | product_1 |
| 2 | product 2 | data of path\to\file2 |
{ color: 'blue' } | 12 | 22 | 1 | product_2 |
How to run logic before the server starts to upgrade the application
You can add a function with name upgrade in the __init__.py file of the app. This function will be called when the server starts and the version in settings.py is different from the current version.
def upgrade(db, from_version, to_version):
# your logic here
pass
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deepsel_cms-1.1.3.tar.gz.
File metadata
- Download URL: deepsel_cms-1.1.3.tar.gz
- Upload date:
- Size: 878.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bddb1d36fd5c485fc1bf67c2c66ed918cf61d72b5bd7544c5794efe7acd9b73e
|
|
| MD5 |
f21e01784d02f8ba44cb791a4b06f879
|
|
| BLAKE2b-256 |
3bdf3336d0c39f9fe6f344fa3bb8b2d18661c292b6bacad641418948ed076d31
|
Provenance
The following attestation bundles were made for deepsel_cms-1.1.3.tar.gz:
Publisher:
publish-backend.yml on DeepselSystems/deepsel-cms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deepsel_cms-1.1.3.tar.gz -
Subject digest:
bddb1d36fd5c485fc1bf67c2c66ed918cf61d72b5bd7544c5794efe7acd9b73e - Sigstore transparency entry: 1196606271
- Sigstore integration time:
-
Permalink:
DeepselSystems/deepsel-cms@50a6177a991a52e1816b34e08077761cb6ee02da -
Branch / Tag:
refs/tags/backend-v1.1.3 - Owner: https://github.com/DeepselSystems
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-backend.yml@50a6177a991a52e1816b34e08077761cb6ee02da -
Trigger Event:
push
-
Statement type:
File details
Details for the file deepsel_cms-1.1.3-py3-none-any.whl.
File metadata
- Download URL: deepsel_cms-1.1.3-py3-none-any.whl
- Upload date:
- Size: 912.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f5626315f67c35abc09b2b38981735870ff1588e55e2a24ff603532374001f6
|
|
| MD5 |
ea1c76fe7e3c83fe1684647a51d035ab
|
|
| BLAKE2b-256 |
6459445f11e6ab845edd203645ba4dcdabf91f968c779e1d778654c49d3eb07e
|
Provenance
The following attestation bundles were made for deepsel_cms-1.1.3-py3-none-any.whl:
Publisher:
publish-backend.yml on DeepselSystems/deepsel-cms
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deepsel_cms-1.1.3-py3-none-any.whl -
Subject digest:
7f5626315f67c35abc09b2b38981735870ff1588e55e2a24ff603532374001f6 - Sigstore transparency entry: 1196606388
- Sigstore integration time:
-
Permalink:
DeepselSystems/deepsel-cms@50a6177a991a52e1816b34e08077761cb6ee02da -
Branch / Tag:
refs/tags/backend-v1.1.3 - Owner: https://github.com/DeepselSystems
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-backend.yml@50a6177a991a52e1816b34e08077761cb6ee02da -
Trigger Event:
push
-
Statement type: