mirror of
https://github.com/sartography/spiff-arena.git
synced 2025-02-05 06:04:32 +00:00
unit tests are passing with the new spec tables
This commit is contained in:
parent
8d67e8cc87
commit
0c6e9a63ba
28
spiffworkflow-backend/migrations/versions/567d22ded3af_.py
Normal file
28
spiffworkflow-backend/migrations/versions/567d22ded3af_.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 567d22ded3af
|
||||||
|
Revises: 2fe2830f45e1
|
||||||
|
Create Date: 2023-03-03 08:38:25.855923
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '567d22ded3af'
|
||||||
|
down_revision = '2fe2830f45e1'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_unique_constraint('bpmn_process_definition_relationship_unique', 'bpmn_process_definition_relationship', ['bpmn_process_definition_parent_id', 'bpmn_process_definition_child_id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('bpmn_process_definition_relationship_unique', 'bpmn_process_definition_relationship', type_='unique')
|
||||||
|
# ### end Alembic commands ###
|
30
spiffworkflow-backend/migrations/versions/6315ff2525b0_.py
Normal file
30
spiffworkflow-backend/migrations/versions/6315ff2525b0_.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 6315ff2525b0
|
||||||
|
Revises: 567d22ded3af
|
||||||
|
Create Date: 2023-03-03 09:19:28.098057
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6315ff2525b0'
|
||||||
|
down_revision = '567d22ded3af'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('process_instance', sa.Column('bpmn_process_definition_id', sa.Integer(), nullable=False))
|
||||||
|
op.create_foreign_key(None, 'process_instance', 'bpmn_process_definition', ['bpmn_process_definition_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'process_instance', type_='foreignkey')
|
||||||
|
op.drop_column('process_instance', 'bpmn_process_definition_id')
|
||||||
|
# ### end Alembic commands ###
|
32
spiffworkflow-backend/migrations/versions/def2cbb0ca6b_.py
Normal file
32
spiffworkflow-backend/migrations/versions/def2cbb0ca6b_.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: def2cbb0ca6b
|
||||||
|
Revises: 6315ff2525b0
|
||||||
|
Create Date: 2023-03-03 09:23:19.480250
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'def2cbb0ca6b'
|
||||||
|
down_revision = '6315ff2525b0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('process_instance', 'bpmn_process_definition_id',
|
||||||
|
existing_type=mysql.INTEGER(),
|
||||||
|
nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('process_instance', 'bpmn_process_definition_id',
|
||||||
|
existing_type=mysql.INTEGER(),
|
||||||
|
nullable=False)
|
||||||
|
# ### end Alembic commands ###
|
@ -21,6 +21,7 @@ class BpmnProcessDefinitionModel(SpiffworkflowBaseDBModel):
|
|||||||
properties_json: str = db.Column(db.JSON, nullable=False)
|
properties_json: str = db.Column(db.JSON, nullable=False)
|
||||||
|
|
||||||
# process or subprocess
|
# process or subprocess
|
||||||
|
# FIXME: will probably ignore for now since we do not strictly need it
|
||||||
type: str = db.Column(db.String(32), nullable=False, index=True)
|
type: str = db.Column(db.String(32), nullable=False, index=True)
|
||||||
|
|
||||||
# TODO: remove these from process_instance
|
# TODO: remove these from process_instance
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from sqlalchemy import UniqueConstraint
|
||||||
from sqlalchemy.orm import deferred
|
from sqlalchemy.orm import deferred
|
||||||
from sqlalchemy import ForeignKey
|
from sqlalchemy import ForeignKey
|
||||||
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
|
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
|
||||||
@ -9,6 +10,12 @@ from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
|||||||
|
|
||||||
class BpmnProcessDefinitionRelationshipModel(SpiffworkflowBaseDBModel):
|
class BpmnProcessDefinitionRelationshipModel(SpiffworkflowBaseDBModel):
|
||||||
__tablename__ = "bpmn_process_definition_relationship"
|
__tablename__ = "bpmn_process_definition_relationship"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"bpmn_process_definition_parent_id", "bpmn_process_definition_child_id", name="bpmn_process_definition_relationship_unique"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
id: int = db.Column(db.Integer, primary_key=True)
|
id: int = db.Column(db.Integer, primary_key=True)
|
||||||
bpmn_process_definition_parent_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=False) # type: ignore
|
bpmn_process_definition_parent_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=False) # type: ignore
|
||||||
bpmn_process_definition_child_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=False) # type: ignore
|
bpmn_process_definition_child_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=False) # type: ignore
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Process_instance."""
|
"""Process_instance."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@ -74,6 +75,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
|
|||||||
)
|
)
|
||||||
process_instance_data = relationship("ProcessInstanceDataModel", cascade="delete")
|
process_instance_data = relationship("ProcessInstanceDataModel", cascade="delete")
|
||||||
|
|
||||||
|
bpmn_process_definition_id: int = db.Column(ForeignKey(BpmnProcessDefinitionModel.id), nullable=True) # type: ignore
|
||||||
|
bpmn_process_definition = relationship(BpmnProcessDefinitionModel)
|
||||||
|
|
||||||
active_human_tasks = relationship(
|
active_human_tasks = relationship(
|
||||||
"HumanTaskModel",
|
"HumanTaskModel",
|
||||||
primaryjoin=(
|
primaryjoin=(
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Process_instance_processor."""
|
"""Process_instance_processor."""
|
||||||
import _strptime # type: ignore
|
import _strptime
|
||||||
|
from spiffworkflow_backend.models import serialized_bpmn_definition # type: ignore
|
||||||
|
from spiffworkflow_backend.models.bpmn_process_definition_relationship import BpmnProcessDefinitionRelationshipModel # noqa: F401
|
||||||
import decimal
|
import decimal
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@ -55,6 +57,7 @@ from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||||
|
from spiffworkflow_backend.models import bpmn_process_definition_relationship
|
||||||
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
|
from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel
|
||||||
from spiffworkflow_backend.models.db import db
|
from spiffworkflow_backend.models.db import db
|
||||||
from spiffworkflow_backend.models.file import File
|
from spiffworkflow_backend.models.file import File
|
||||||
@ -526,9 +529,32 @@ class ProcessInstanceProcessor:
|
|||||||
def _get_full_bpmn_json(cls, process_instance_model: ProcessInstanceModel) -> dict:
|
def _get_full_bpmn_json(cls, process_instance_model: ProcessInstanceModel) -> dict:
|
||||||
if process_instance_model.serialized_bpmn_definition_id is None:
|
if process_instance_model.serialized_bpmn_definition_id is None:
|
||||||
return {}
|
return {}
|
||||||
serialized_bpmn_definition = process_instance_model.serialized_bpmn_definition
|
# serialized_bpmn_definition = process_instance_model.serialized_bpmn_definition
|
||||||
|
# print(f"serialized_bpmn_definition.static_json: {serialized_bpmn_definition.static_json}")
|
||||||
|
# loaded_json: dict = json.loads(serialized_bpmn_definition.static_json) # or "{}")
|
||||||
|
|
||||||
|
serialized_bpmn_definition = {}
|
||||||
|
bpmn_process_definition = BpmnProcessDefinitionModel.query.filter_by(id=process_instance_model.bpmn_process_definition_id).first()
|
||||||
|
task_definitions = TaskDefinitionModel.query.filter_by(bpmn_process_definition_id=process_instance_model.bpmn_process_definition_id).all()
|
||||||
|
if bpmn_process_definition is not None:
|
||||||
|
serialized_bpmn_definition = {"serializer_version": cls.SERIALIZER_VERSION, "spec": {}, "subprocess_specs": {}}
|
||||||
|
bpmn_process_definition_dict = json.loads(bpmn_process_definition.properties_json)
|
||||||
|
bpmn_process_definition_dict['task_specs'] = {}
|
||||||
|
for task_definition in task_definitions:
|
||||||
|
bpmn_process_definition_dict['task_specs'][task_definition.bpmn_identifier] = json.loads(task_definition.properties_json)
|
||||||
|
serialized_bpmn_definition['spec'] = bpmn_process_definition_dict
|
||||||
|
|
||||||
|
bpmn_process_subprocess_definitions = BpmnProcessDefinitionRelationshipModel.query.filter_by(bpmn_process_definition_parent_id=bpmn_process_definition.id).all()
|
||||||
|
for bpmn_process_subprocess_definition in bpmn_process_subprocess_definitions:
|
||||||
|
subprocess_task_definitions = TaskDefinitionModel.query.filter_by(bpmn_process_definition_id=bpmn_process_subprocess_definition.id).all()
|
||||||
|
bpmn_process_subprocess_dict = json.loads(bpmn_process_definition.properties_json)
|
||||||
|
bpmn_process_subprocess_dict['task_specs'] = {}
|
||||||
|
for subprocess_task_definition in subprocess_task_definitions:
|
||||||
|
bpmn_process_subprocess_dict['task_specs'][subprocess_task_definition.bpmn_identifier] = json.loads(subprocess_task_definition.properties_json)
|
||||||
|
serialized_bpmn_definition['subprocess_specs'][bpmn_process_subprocess_definition.bpmn_identifier] = bpmn_process_subprocess_dict
|
||||||
|
loaded_json: dict = serialized_bpmn_definition
|
||||||
|
|
||||||
process_instance_data = process_instance_model.process_instance_data
|
process_instance_data = process_instance_model.process_instance_data
|
||||||
loaded_json: dict = json.loads(serialized_bpmn_definition.static_json or "{}")
|
|
||||||
loaded_json.update(json.loads(process_instance_data.runtime_json))
|
loaded_json.update(json.loads(process_instance_data.runtime_json))
|
||||||
return loaded_json
|
return loaded_json
|
||||||
|
|
||||||
@ -909,19 +935,15 @@ class ProcessInstanceProcessor:
|
|||||||
self.process_instance_model.process_instance_data = process_instance_data
|
self.process_instance_model.process_instance_data = process_instance_data
|
||||||
|
|
||||||
|
|
||||||
def _store_bpmn_process_definitions(self, process_bpmn_properties: dict) -> None:
|
def _store_bpmn_process_definition(self, process_bpmn_properties: dict, bpmn_process_definition_parent: Optional[BpmnProcessDefinitionModel] = None) -> BpmnProcessDefinitionModel:
|
||||||
# for process_bpmn_identifier, process_bpmn_properties in bpmn_spec_dict.items():
|
|
||||||
print(f"process_bpmn_properties: {process_bpmn_properties}")
|
|
||||||
process_bpmn_identifier = process_bpmn_properties['name']
|
process_bpmn_identifier = process_bpmn_properties['name']
|
||||||
new_hash_digest = sha256(
|
new_hash_digest = sha256(
|
||||||
json.dumps(process_bpmn_properties, sort_keys=True).encode("utf8")
|
json.dumps(process_bpmn_properties, sort_keys=True).encode("utf8")
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
bpmn_process_definition = BpmnProcessDefinitionModel.query.filter_by(
|
bpmn_process_definition: Optional[BpmnProcessDefinitionModel] = BpmnProcessDefinitionModel.query.filter_by(
|
||||||
hash=new_hash_digest
|
hash=new_hash_digest
|
||||||
).first()
|
).first()
|
||||||
if bpmn_process_definition is None:
|
if bpmn_process_definition is None:
|
||||||
# print(f"process_bpmn_identifier: {process_bpmn_identifier}")
|
|
||||||
print(f"process_bpmn_properties: {process_bpmn_properties}")
|
|
||||||
task_specs = process_bpmn_properties.pop("task_specs")
|
task_specs = process_bpmn_properties.pop("task_specs")
|
||||||
bpmn_process_definition = BpmnProcessDefinitionModel(
|
bpmn_process_definition = BpmnProcessDefinitionModel(
|
||||||
hash=new_hash_digest, bpmn_identifier=process_bpmn_identifier, properties_json=json.dumps(process_bpmn_properties), type="process"
|
hash=new_hash_digest, bpmn_identifier=process_bpmn_identifier, properties_json=json.dumps(process_bpmn_properties), type="process"
|
||||||
@ -937,6 +959,19 @@ class ProcessInstanceProcessor:
|
|||||||
)
|
)
|
||||||
db.session.add(task_definition)
|
db.session.add(task_definition)
|
||||||
|
|
||||||
|
if bpmn_process_definition_parent:
|
||||||
|
bpmn_process_definition_relationship = BpmnProcessDefinitionRelationshipModel.query.filter_by(
|
||||||
|
bpmn_process_definition_parent_id=bpmn_process_definition_parent.id,
|
||||||
|
bpmn_process_definition_child_id=bpmn_process_definition.id,
|
||||||
|
).first()
|
||||||
|
if bpmn_process_definition_relationship is None:
|
||||||
|
bpmn_process_definition_relationship = BpmnProcessDefinitionRelationshipModel(
|
||||||
|
bpmn_process_definition_parent_id=bpmn_process_definition_parent.id,
|
||||||
|
bpmn_process_definition_child_id=bpmn_process_definition.id,
|
||||||
|
)
|
||||||
|
db.session.add(bpmn_process_definition_relationship)
|
||||||
|
return bpmn_process_definition
|
||||||
|
|
||||||
def _add_bpmn_json_records_new(self) -> None:
|
def _add_bpmn_json_records_new(self) -> None:
|
||||||
"""Adds serialized_bpmn_definition and process_instance_data records to the db session.
|
"""Adds serialized_bpmn_definition and process_instance_data records to the db session.
|
||||||
|
|
||||||
@ -952,7 +987,11 @@ class ProcessInstanceProcessor:
|
|||||||
else:
|
else:
|
||||||
process_instance_data_dict[bpmn_key] = bpmn_dict[bpmn_key]
|
process_instance_data_dict[bpmn_key] = bpmn_dict[bpmn_key]
|
||||||
|
|
||||||
self._store_bpmn_process_definitions(bpmn_spec_dict['spec'])
|
bpmn_process_definition_parent = self._store_bpmn_process_definition(bpmn_spec_dict['spec'])
|
||||||
|
for process_bpmn_properties in bpmn_spec_dict['subprocess_specs'].values():
|
||||||
|
self._store_bpmn_process_definition(process_bpmn_properties, bpmn_process_definition_parent)
|
||||||
|
self.process_instance_model.bpmn_process_definition = bpmn_process_definition_parent
|
||||||
|
|
||||||
|
|
||||||
# # FIXME: always save new hash until we get updated Spiff without loopresettask
|
# # FIXME: always save new hash until we get updated Spiff without loopresettask
|
||||||
# # if self.process_instance_model.serialized_bpmn_definition_id is None:
|
# # if self.process_instance_model.serialized_bpmn_definition_id is None:
|
||||||
|
@ -24,14 +24,14 @@
|
|||||||
</bpmn:correlationPropertyRetrievalExpression>
|
</bpmn:correlationPropertyRetrievalExpression>
|
||||||
</bpmn:correlationProperty>
|
</bpmn:correlationProperty>
|
||||||
<bpmn:process id="test_dot_notation" name="Test Dot Notation" isExecutable="true">
|
<bpmn:process id="test_dot_notation" name="Test Dot Notation" isExecutable="true">
|
||||||
<bpmn:startEvent id="start" name="Start">
|
<bpmn:startEvent id="startHere" name="StartHere">
|
||||||
<bpmn:outgoing>Flow_0dbnzbi</bpmn:outgoing>
|
<bpmn:outgoing>Flow_0dbnzbi</bpmn:outgoing>
|
||||||
</bpmn:startEvent>
|
</bpmn:startEvent>
|
||||||
<bpmn:sequenceFlow id="Flow_0dbnzbi" sourceRef="start" targetRef="get_data" />
|
<bpmn:sequenceFlow id="Flow_0dbnzbi" sourceRef="startHere" targetRef="get_data" />
|
||||||
<bpmn:endEvent id="end" name="End">
|
<bpmn:endEvent id="endHere" name="End">
|
||||||
<bpmn:incoming>Flow_0nt355i</bpmn:incoming>
|
<bpmn:incoming>Flow_0nt355i</bpmn:incoming>
|
||||||
</bpmn:endEvent>
|
</bpmn:endEvent>
|
||||||
<bpmn:sequenceFlow id="Flow_0nt355i" sourceRef="get_data" targetRef="end" />
|
<bpmn:sequenceFlow id="Flow_0nt355i" sourceRef="get_data" targetRef="endHere" />
|
||||||
<bpmn:userTask id="get_data" name="Get Data">
|
<bpmn:userTask id="get_data" name="Get Data">
|
||||||
<bpmn:extensionElements>
|
<bpmn:extensionElements>
|
||||||
<spiffworkflow:properties>
|
<spiffworkflow:properties>
|
||||||
@ -53,13 +53,13 @@
|
|||||||
<di:waypoint x="360" y="230" />
|
<di:waypoint x="360" y="230" />
|
||||||
<di:waypoint x="412" y="230" />
|
<di:waypoint x="412" y="230" />
|
||||||
</bpmndi:BPMNEdge>
|
</bpmndi:BPMNEdge>
|
||||||
<bpmndi:BPMNShape id="Event_1uf4njx_di" bpmnElement="start">
|
<bpmndi:BPMNShape id="Event_1uf4njx_di" bpmnElement="startHere">
|
||||||
<dc:Bounds x="172" y="212" width="36" height="36" />
|
<dc:Bounds x="172" y="212" width="36" height="36" />
|
||||||
<bpmndi:BPMNLabel>
|
<bpmndi:BPMNLabel>
|
||||||
<dc:Bounds x="178" y="255" width="24" height="14" />
|
<dc:Bounds x="178" y="255" width="24" height="14" />
|
||||||
</bpmndi:BPMNLabel>
|
</bpmndi:BPMNLabel>
|
||||||
</bpmndi:BPMNShape>
|
</bpmndi:BPMNShape>
|
||||||
<bpmndi:BPMNShape id="Event_00d0dwr_di" bpmnElement="end">
|
<bpmndi:BPMNShape id="Event_00d0dwr_di" bpmnElement="endHere">
|
||||||
<dc:Bounds x="412" y="212" width="36" height="36" />
|
<dc:Bounds x="412" y="212" width="36" height="36" />
|
||||||
<bpmndi:BPMNLabel>
|
<bpmndi:BPMNLabel>
|
||||||
<dc:Bounds x="420" y="255" width="20" height="14" />
|
<dc:Bounds x="420" y="255" width="20" height="14" />
|
||||||
|
@ -1301,10 +1301,11 @@ class TestProcessApi(BaseTest):
|
|||||||
assert create_response.json is not None
|
assert create_response.json is not None
|
||||||
assert create_response.status_code == 201
|
assert create_response.status_code == 201
|
||||||
process_instance_id = create_response.json["id"]
|
process_instance_id = create_response.json["id"]
|
||||||
client.post(
|
run_response = client.post(
|
||||||
f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}/run",
|
f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}/run",
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
)
|
)
|
||||||
|
assert run_response.status_code == 200
|
||||||
show_response = client.get(
|
show_response = client.get(
|
||||||
f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}?process_identifier={spec_reference.identifier}",
|
f"/v1.0/process-instances/{modified_process_model_identifier}/{process_instance_id}?process_identifier={spec_reference.identifier}",
|
||||||
headers=self.logged_in_headers(with_super_admin_user),
|
headers=self.logged_in_headers(with_super_admin_user),
|
||||||
|
@ -13,9 +13,7 @@ from spiffworkflow_backend.services.process_instance_service import (
|
|||||||
|
|
||||||
|
|
||||||
class TestDotNotation(BaseTest):
|
class TestDotNotation(BaseTest):
|
||||||
"""TestVariousBpmnConstructs."""
|
def test_dot_notation_in_message_path(
|
||||||
|
|
||||||
def test_dot_notation(
|
|
||||||
self,
|
self,
|
||||||
app: Flask,
|
app: Flask,
|
||||||
client: FlaskClient,
|
client: FlaskClient,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user