orchestrator-core 4.7.1__py3-none-any.whl → 4.7.2rc1__py3-none-any.whl

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.
orchestrator/__init__.py CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  """This is the orchestrator workflow engine."""
15
15
 
16
- __version__ = "4.7.1"
16
+ __version__ = "4.7.2rc1"
17
17
 
18
18
 
19
19
  from structlog import get_logger
@@ -16,6 +16,7 @@ from fastapi import APIRouter, HTTPException, Query, status
16
16
 
17
17
  from orchestrator.db import db
18
18
  from orchestrator.schemas.search import (
19
+ CursorInfoSchema,
19
20
  ExportResponse,
20
21
  PageInfoSchema,
21
22
  PathsResponse,
@@ -81,10 +82,24 @@ async def _perform_search_and_fetch(
81
82
 
82
83
  next_page_cursor = encode_next_page_cursor(search_response, page_cursor, query)
83
84
  has_next_page = next_page_cursor is not None
84
- page_info = PageInfoSchema(has_next_page=has_next_page, next_page_cursor=next_page_cursor)
85
+ page_info = PageInfoSchema(
86
+ has_next_page=has_next_page,
87
+ next_page_cursor=next_page_cursor,
88
+ )
89
+
90
+ cursor_info = None
91
+ if search_response.total_items:
92
+ cursor_info = CursorInfoSchema(
93
+ total_items=search_response.total_items,
94
+ start_cursor=search_response.start_cursor,
95
+ end_cursor=search_response.end_cursor,
96
+ )
85
97
 
86
98
  return SearchResultsSchema(
87
- data=search_response.results, page_info=page_info, search_metadata=search_response.metadata
99
+ data=search_response.results,
100
+ page_info=page_info,
101
+ search_metadata=search_response.metadata,
102
+ cursor=cursor_info,
88
103
  )
89
104
  except (InvalidCursorError, ValueError) as e:
90
105
  raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
orchestrator/db/models.py CHANGED
@@ -38,7 +38,6 @@ from sqlalchemy import (
38
38
  Select,
39
39
  String,
40
40
  Table,
41
- Text,
42
41
  TypeDecorator,
43
42
  UniqueConstraint,
44
43
  select,
@@ -69,6 +68,37 @@ logger = structlog.get_logger(__name__)
69
68
  TAG_LENGTH = 20
70
69
  STATUS_LENGTH = 255
71
70
 
71
+ # Field length limits chosen based on expected usage patterns
72
+ # These values are intended to be reasonable, but give lots of wiggle room
73
+ # If these values are updated, they also need to be updated in a migration, as in migration d69e10434a04
74
+ NOTE_LENGTH = 5000
75
+ DESCRIPTION_LENGTH = 2000
76
+ FAILED_REASON_LENGTH = 10000
77
+ TRACEBACK_LENGTH = 50000
78
+ RESOURCE_VALUE_LENGTH = 10000
79
+ DOMAIN_MODEL_ATTR_LENGTH = 255
80
+
81
+
82
+ class StringThatAutoConvertsToNullWhenEmpty(TypeDecorator):
83
+ """A String type that converts empty strings to NULL on save."""
84
+
85
+ impl = String
86
+ cache_ok = True
87
+ python_type = str
88
+
89
+ def __init__(self, length: int | None = None):
90
+ super().__init__(length)
91
+
92
+ def process_bind_param(self, value: str | None, dialect: Dialect) -> str | None:
93
+ """Called when saving to DB - convert empty/whitespace to NULL."""
94
+ if value is not None and value.strip() == "":
95
+ return None
96
+ return value
97
+
98
+ def process_result_value(self, value: str | None, dialect: Dialect) -> str | None:
99
+ """Called when loading from DB - return as-is."""
100
+ return value
101
+
72
102
 
73
103
  class UtcTimestampError(Exception, DontWrapMixin):
74
104
  pass
@@ -122,8 +152,8 @@ class ProcessTable(BaseModel):
122
152
  last_modified_at = mapped_column(
123
153
  UtcTimestamp, server_default=text("current_timestamp()"), onupdate=nowtz, nullable=False
124
154
  )
125
- failed_reason = mapped_column(Text())
126
- traceback = mapped_column(Text())
155
+ failed_reason = mapped_column(String(FAILED_REASON_LENGTH))
156
+ traceback = mapped_column(String(TRACEBACK_LENGTH))
127
157
  created_by = mapped_column(String(255), nullable=True)
128
158
  is_task = mapped_column(Boolean, nullable=False, server_default=text("false"), index=True)
129
159
 
@@ -223,7 +253,7 @@ class ProductTable(BaseModel):
223
253
 
224
254
  product_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
225
255
  name = mapped_column(String(), nullable=False, unique=True)
226
- description = mapped_column(Text(), nullable=False)
256
+ description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
227
257
  product_type = mapped_column(String(255), nullable=False)
228
258
  tag = mapped_column(String(TAG_LENGTH), nullable=False, index=True)
229
259
  status = mapped_column(String(STATUS_LENGTH), nullable=False)
@@ -302,7 +332,7 @@ class ProductBlockTable(BaseModel):
302
332
 
303
333
  product_block_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
304
334
  name = mapped_column(String(), nullable=False, unique=True)
305
- description = mapped_column(Text(), nullable=False)
335
+ description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
306
336
  tag = mapped_column(String(TAG_LENGTH))
307
337
  status = mapped_column(String(STATUS_LENGTH))
308
338
  created_at = mapped_column(UtcTimestamp, nullable=False, server_default=text("current_timestamp()"))
@@ -401,7 +431,7 @@ class ResourceTypeTable(BaseModel):
401
431
 
402
432
  resource_type_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
403
433
  resource_type = mapped_column(String(510), nullable=False, unique=True)
404
- description = mapped_column(Text())
434
+ description = mapped_column(String(DESCRIPTION_LENGTH))
405
435
 
406
436
  product_blocks = relationship(
407
437
  "ProductBlockTable", secondary=product_block_resource_type_association, back_populates="resource_types"
@@ -414,7 +444,7 @@ class WorkflowTable(BaseModel):
414
444
  workflow_id = mapped_column(UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True)
415
445
  name = mapped_column(String(), nullable=False, unique=True)
416
446
  target = mapped_column(String(), nullable=False)
417
- description = mapped_column(Text(), nullable=False)
447
+ description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
418
448
  created_at = mapped_column(UtcTimestamp, nullable=False, server_default=text("current_timestamp()"))
419
449
  deleted_at = mapped_column(UtcTimestamp, deferred=True)
420
450
 
@@ -452,7 +482,7 @@ class SubscriptionInstanceRelationTable(BaseModel):
452
482
 
453
483
  # Needed to make sure subscription instance is populated in the right domain model attribute, if more than one
454
484
  # attribute uses the same product block model.
455
- domain_model_attr = Column(Text())
485
+ domain_model_attr = Column(String(DOMAIN_MODEL_ATTR_LENGTH))
456
486
 
457
487
  in_use_by: Mapped[SubscriptionInstanceTable] = relationship(
458
488
  "SubscriptionInstanceTable", back_populates="depends_on_block_relations", foreign_keys=[in_use_by_id]
@@ -559,7 +589,7 @@ class SubscriptionInstanceValueTable(BaseModel):
559
589
  resource_type_id = mapped_column(
560
590
  UUIDType, ForeignKey("resource_types.resource_type_id"), nullable=False, index=True
561
591
  )
562
- value = mapped_column(Text(), nullable=False)
592
+ value = mapped_column(String(RESOURCE_VALUE_LENGTH), nullable=False)
563
593
 
564
594
  resource_type = relationship("ResourceTypeTable", lazy="subquery")
565
595
  subscription_instance = relationship("SubscriptionInstanceTable", back_populates="values")
@@ -587,7 +617,7 @@ class SubscriptionCustomerDescriptionTable(BaseModel):
587
617
  index=True,
588
618
  )
589
619
  customer_id = mapped_column(String, nullable=False, index=True)
590
- description = mapped_column(Text(), nullable=False)
620
+ description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
591
621
  created_at = mapped_column(UtcTimestamp, nullable=False, server_default=text("current_timestamp()"))
592
622
  version = mapped_column(Integer, nullable=False, server_default="1")
593
623
 
@@ -600,14 +630,14 @@ class SubscriptionTable(BaseModel):
600
630
  subscription_id = mapped_column(
601
631
  UUIDType, server_default=text("uuid_generate_v4()"), primary_key=True, nullable=False
602
632
  )
603
- description = mapped_column(Text(), nullable=False)
633
+ description = mapped_column(String(DESCRIPTION_LENGTH), nullable=False)
604
634
  status = mapped_column(String(STATUS_LENGTH), nullable=False, index=True)
605
635
  product_id = mapped_column(UUIDType, ForeignKey("products.product_id"), nullable=False, index=True)
606
636
  customer_id = mapped_column(String, index=True, nullable=False)
607
637
  insync = mapped_column(Boolean(), nullable=False)
608
638
  start_date = mapped_column(UtcTimestamp, nullable=True)
609
639
  end_date = mapped_column(UtcTimestamp)
610
- note = mapped_column(Text())
640
+ note = mapped_column(StringThatAutoConvertsToNullWhenEmpty(NOTE_LENGTH))
611
641
  version = mapped_column(Integer, nullable=False, server_default="1")
612
642
 
613
643
  product = relationship("ProductTable", foreign_keys=[product_id])
@@ -0,0 +1,32 @@
1
+ """Convert unlimited text fields to limited nullable strings and normalize empty subscription notes.
2
+
3
+ Revision ID: d69e10434a04
4
+ Revises: 9736496e3eba
5
+ Create Date: 2026-01-12 14:17:58.255515
6
+
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ import sqlalchemy as sa
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision = "d69e10434a04"
16
+ down_revision = "9736496e3eba"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ conn = op.get_bind()
23
+ revision_file_path = Path(__file__.replace(".py", "_upgrade.sql"))
24
+ with open(revision_file_path) as f:
25
+ conn.execute(sa.text(f.read()))
26
+
27
+
28
+ def downgrade() -> None:
29
+ conn = op.get_bind()
30
+ revision_file_path = Path(__file__.replace(".py", "_downgrade.sql"))
31
+ with open(revision_file_path) as f:
32
+ conn.execute(sa.text(f.read()))
@@ -0,0 +1,138 @@
1
+ -- Downgrade: Revert limited nullable strings back to unlimited text fields.
2
+ -- Revision ID: d69e10434a04
3
+
4
+ -- Drop triggers first
5
+ DROP TRIGGER IF EXISTS fi_refresh_search ON fixed_inputs;
6
+ DROP TRIGGER IF EXISTS products_refresh_search ON products;
7
+ DROP TRIGGER IF EXISTS sub_cust_desc_refresh_search ON subscription_customer_descriptions;
8
+ DROP TRIGGER IF EXISTS siv_refresh_search ON subscription_instance_values;
9
+ DROP TRIGGER IF EXISTS sub_refresh_search ON subscriptions;
10
+
11
+ -- Drop the refresh function
12
+ DROP FUNCTION IF EXISTS refresh_subscriptions_search_view();
13
+
14
+ -- Drop the materialized view (CASCADE will drop indexes)
15
+ DROP MATERIALIZED VIEW IF EXISTS subscriptions_search CASCADE;
16
+
17
+ -- Alter column types back from VARCHAR to TEXT
18
+ ALTER TABLE subscriptions ALTER COLUMN note TYPE TEXT;
19
+ ALTER TABLE subscriptions ALTER COLUMN description TYPE TEXT;
20
+ ALTER TABLE processes ALTER COLUMN failed_reason TYPE TEXT;
21
+ ALTER TABLE processes ALTER COLUMN traceback TYPE TEXT;
22
+ ALTER TABLE products ALTER COLUMN description TYPE TEXT;
23
+ ALTER TABLE product_blocks ALTER COLUMN description TYPE TEXT;
24
+ ALTER TABLE resource_types ALTER COLUMN description TYPE TEXT;
25
+ ALTER TABLE workflows ALTER COLUMN description TYPE TEXT;
26
+ ALTER TABLE subscription_customer_descriptions ALTER COLUMN description TYPE TEXT;
27
+ ALTER TABLE subscription_instance_values ALTER COLUMN value TYPE TEXT;
28
+ ALTER TABLE subscription_instance_relations ALTER COLUMN domain_model_attr TYPE TEXT;
29
+
30
+ -- Recreate the materialized view
31
+ CREATE MATERIALIZED VIEW subscriptions_search AS
32
+ WITH rt_info AS (SELECT s.subscription_id,
33
+ concat_ws(
34
+ ' ',
35
+ string_agg(rt.resource_type || ' ' || siv.value, ' '),
36
+ string_agg(distinct 'subscription_instance_id' || ':' || si.subscription_instance_id, ' ')
37
+ ) AS rt_info
38
+ FROM subscription_instance_values siv
39
+ JOIN resource_types rt ON siv.resource_type_id = rt.resource_type_id
40
+ JOIN subscription_instances si ON siv.subscription_instance_id = si.subscription_instance_id
41
+ JOIN subscriptions s ON si.subscription_id = s.subscription_id
42
+ GROUP BY s.subscription_id),
43
+ sub_prod_info AS (SELECT s.subscription_id,
44
+ array_to_string(
45
+ ARRAY ['subscription_id:' || s.subscription_id,
46
+ 'status:' || s.status,
47
+ 'insync:' || s.insync,
48
+ 'subscription_description:' || s.description,
49
+ 'note:' || coalesce(s.note, ''),
50
+ 'customer_id:' || s.customer_id,
51
+ 'product_id:' || s.product_id],
52
+ ' '
53
+ ) AS sub_info,
54
+ array_to_string(
55
+ ARRAY ['product_name:' || p.name,
56
+ 'product_description:' || p.description,
57
+ 'tag:' || p.tag,
58
+ 'product_type:', p.product_type],
59
+ ' '
60
+ ) AS prod_info
61
+ FROM subscriptions s
62
+ JOIN products p ON s.product_id = p.product_id),
63
+ fi_info AS (SELECT s.subscription_id,
64
+ string_agg(fi.name || ':' || fi.value, ' ') AS fi_info
65
+ FROM subscriptions s
66
+ JOIN products p ON s.product_id = p.product_id
67
+ JOIN fixed_inputs fi ON p.product_id = fi.product_id
68
+ GROUP BY s.subscription_id),
69
+ cust_info AS (SELECT s.subscription_id,
70
+ string_agg('customer_description: ' || scd.description, ' ') AS cust_info
71
+ FROM subscriptions s
72
+ JOIN subscription_customer_descriptions scd ON s.subscription_id = scd.subscription_id
73
+ GROUP BY s.subscription_id)
74
+ -- to_tsvector handles parsing of hyphened words in a peculiar way and is inconsistent with how to_tsquery parses it in Postgres <14
75
+ -- Replacing all hyphens with underscores makes the parsing more predictable and removes some issues arising when searching for subscription ids for example
76
+ -- See: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=0c4f355c6a5fd437f71349f2f3d5d491382572b7
77
+ SELECT s.subscription_id,
78
+ to_tsvector(
79
+ 'simple',
80
+ replace(
81
+ concat_ws(
82
+ ' ',
83
+ spi.sub_info,
84
+ spi.prod_info,
85
+ fi.fi_info,
86
+ rti.rt_info,
87
+ ci.cust_info,
88
+ md.metadata::text
89
+ ),
90
+ '-', '_')
91
+ ) as tsv
92
+ FROM subscriptions s
93
+ LEFT JOIN sub_prod_info spi ON s.subscription_id = spi.subscription_id
94
+ LEFT JOIN fi_info fi ON s.subscription_id = fi.subscription_id
95
+ LEFT JOIN rt_info rti ON s.subscription_id = rti.subscription_id
96
+ LEFT JOIN cust_info ci ON s.subscription_id = ci.subscription_id
97
+ LEFT JOIN subscription_metadata md ON s.subscription_id = md.subscription_id;
98
+
99
+ -- Create indexes
100
+ CREATE INDEX subscriptions_search_tsv_idx ON subscriptions_search USING GIN (tsv);
101
+ CREATE UNIQUE INDEX subscriptions_search_subscription_id_idx ON subscriptions_search (subscription_id);
102
+
103
+ -- Create refresh function with epoch-based throttling
104
+ CREATE OR REPLACE FUNCTION refresh_subscriptions_search_view()
105
+ RETURNS TRIGGER
106
+ LANGUAGE plpgsql
107
+ AS
108
+ $$
109
+ DECLARE
110
+ should_refresh bool;
111
+ current_epoch int;
112
+ last_refresh_epoch int;
113
+ comment_sql text;
114
+ BEGIN
115
+ SELECT extract(epoch from now())::int INTO current_epoch;
116
+ SELECT coalesce(pg_catalog.obj_description('subscriptions_search'::regclass)::int, 0) INTO last_refresh_epoch;
117
+
118
+ SELECT (current_epoch - last_refresh_epoch) > 120 INTO should_refresh;
119
+
120
+ IF should_refresh THEN
121
+ REFRESH MATERIALIZED VIEW CONCURRENTLY subscriptions_search;
122
+
123
+ comment_sql := 'COMMENT ON MATERIALIZED VIEW subscriptions_search IS ' || quote_literal(current_epoch);
124
+ EXECUTE comment_sql;
125
+ END IF;
126
+ RETURN NULL;
127
+ END;
128
+ $$;
129
+
130
+ -- Create triggers
131
+ CREATE CONSTRAINT TRIGGER fi_refresh_search AFTER UPDATE ON fixed_inputs DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
132
+ CREATE CONSTRAINT TRIGGER products_refresh_search AFTER UPDATE ON products DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
133
+ CREATE CONSTRAINT TRIGGER sub_cust_desc_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_customer_descriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
134
+ CREATE CONSTRAINT TRIGGER siv_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_instance_values DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
135
+ CREATE CONSTRAINT TRIGGER sub_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
136
+
137
+ -- Refresh the materialized view
138
+ REFRESH MATERIALIZED VIEW subscriptions_search;
@@ -0,0 +1,153 @@
1
+ -- Convert unlimited text fields to limited nullable strings and normalize empty subscription notes.
2
+ -- Revision ID: d69e10434a04
3
+
4
+ -- Field length limits chosen based on expected usage patterns
5
+ -- These values are intended to be reasonable, but give lots of wiggle room
6
+ -- If these values are updated, they also need to be updated in orchestrator-core/orchestrator/db/models.py
7
+ -- NOTE_LENGTH = 5000
8
+ -- DESCRIPTION_LENGTH = 2000
9
+ -- FAILED_REASON_LENGTH = 10000
10
+ -- TRACEBACK_LENGTH = 50000
11
+ -- RESOURCE_VALUE_LENGTH = 10000
12
+ -- DOMAIN_MODEL_ATTR_LENGTH = 255
13
+
14
+ -- Drop triggers first
15
+ DROP TRIGGER IF EXISTS fi_refresh_search ON fixed_inputs;
16
+ DROP TRIGGER IF EXISTS products_refresh_search ON products;
17
+ DROP TRIGGER IF EXISTS sub_cust_desc_refresh_search ON subscription_customer_descriptions;
18
+ DROP TRIGGER IF EXISTS siv_refresh_search ON subscription_instance_values;
19
+ DROP TRIGGER IF EXISTS sub_refresh_search ON subscriptions;
20
+
21
+ -- Drop the refresh function
22
+ DROP FUNCTION IF EXISTS refresh_subscriptions_search_view();
23
+
24
+ -- Drop the materialized view (CASCADE will drop indexes)
25
+ DROP MATERIALIZED VIEW IF EXISTS subscriptions_search CASCADE;
26
+
27
+ -- Normalize empty strings to NULL before altering column types
28
+ UPDATE subscriptions
29
+ SET note = NULL
30
+ WHERE note IS NOT NULL AND TRIM(note) = '';
31
+
32
+ -- Alter column types from TEXT to VARCHAR with limits
33
+ ALTER TABLE subscriptions ALTER COLUMN note TYPE VARCHAR(5000);
34
+ ALTER TABLE subscriptions ALTER COLUMN description TYPE VARCHAR(2000);
35
+ ALTER TABLE processes ALTER COLUMN failed_reason TYPE VARCHAR(10000);
36
+ ALTER TABLE processes ALTER COLUMN traceback TYPE VARCHAR(50000);
37
+ ALTER TABLE products ALTER COLUMN description TYPE VARCHAR(2000);
38
+ ALTER TABLE product_blocks ALTER COLUMN description TYPE VARCHAR(2000);
39
+ ALTER TABLE resource_types ALTER COLUMN description TYPE VARCHAR(2000);
40
+ ALTER TABLE workflows ALTER COLUMN description TYPE VARCHAR(2000);
41
+ ALTER TABLE subscription_customer_descriptions ALTER COLUMN description TYPE VARCHAR(2000);
42
+ ALTER TABLE subscription_instance_values ALTER COLUMN value TYPE VARCHAR(10000);
43
+ ALTER TABLE subscription_instance_relations ALTER COLUMN domain_model_attr TYPE VARCHAR(255);
44
+
45
+ -- Recreate the materialized view
46
+ CREATE MATERIALIZED VIEW subscriptions_search AS
47
+ WITH rt_info AS (SELECT s.subscription_id,
48
+ concat_ws(
49
+ ' ',
50
+ string_agg(rt.resource_type || ' ' || siv.value, ' '),
51
+ string_agg(distinct 'subscription_instance_id' || ':' || si.subscription_instance_id, ' ')
52
+ ) AS rt_info
53
+ FROM subscription_instance_values siv
54
+ JOIN resource_types rt ON siv.resource_type_id = rt.resource_type_id
55
+ JOIN subscription_instances si ON siv.subscription_instance_id = si.subscription_instance_id
56
+ JOIN subscriptions s ON si.subscription_id = s.subscription_id
57
+ GROUP BY s.subscription_id),
58
+ sub_prod_info AS (SELECT s.subscription_id,
59
+ array_to_string(
60
+ ARRAY ['subscription_id:' || s.subscription_id,
61
+ 'status:' || s.status,
62
+ 'insync:' || s.insync,
63
+ 'subscription_description:' || s.description,
64
+ 'note:' || coalesce(s.note, ''),
65
+ 'customer_id:' || s.customer_id,
66
+ 'product_id:' || s.product_id],
67
+ ' '
68
+ ) AS sub_info,
69
+ array_to_string(
70
+ ARRAY ['product_name:' || p.name,
71
+ 'product_description:' || p.description,
72
+ 'tag:' || p.tag,
73
+ 'product_type:', p.product_type],
74
+ ' '
75
+ ) AS prod_info
76
+ FROM subscriptions s
77
+ JOIN products p ON s.product_id = p.product_id),
78
+ fi_info AS (SELECT s.subscription_id,
79
+ string_agg(fi.name || ':' || fi.value, ' ') AS fi_info
80
+ FROM subscriptions s
81
+ JOIN products p ON s.product_id = p.product_id
82
+ JOIN fixed_inputs fi ON p.product_id = fi.product_id
83
+ GROUP BY s.subscription_id),
84
+ cust_info AS (SELECT s.subscription_id,
85
+ string_agg('customer_description: ' || scd.description, ' ') AS cust_info
86
+ FROM subscriptions s
87
+ JOIN subscription_customer_descriptions scd ON s.subscription_id = scd.subscription_id
88
+ GROUP BY s.subscription_id)
89
+ -- to_tsvector handles parsing of hyphened words in a peculiar way and is inconsistent with how to_tsquery parses it in Postgres <14
90
+ -- Replacing all hyphens with underscores makes the parsing more predictable and removes some issues arising when searching for subscription ids for example
91
+ -- See: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=0c4f355c6a5fd437f71349f2f3d5d491382572b7
92
+ SELECT s.subscription_id,
93
+ to_tsvector(
94
+ 'simple',
95
+ replace(
96
+ concat_ws(
97
+ ' ',
98
+ spi.sub_info,
99
+ spi.prod_info,
100
+ fi.fi_info,
101
+ rti.rt_info,
102
+ ci.cust_info,
103
+ md.metadata::text
104
+ ),
105
+ '-', '_')
106
+ ) as tsv
107
+ FROM subscriptions s
108
+ LEFT JOIN sub_prod_info spi ON s.subscription_id = spi.subscription_id
109
+ LEFT JOIN fi_info fi ON s.subscription_id = fi.subscription_id
110
+ LEFT JOIN rt_info rti ON s.subscription_id = rti.subscription_id
111
+ LEFT JOIN cust_info ci ON s.subscription_id = ci.subscription_id
112
+ LEFT JOIN subscription_metadata md ON s.subscription_id = md.subscription_id;
113
+
114
+ -- Create indexes
115
+ CREATE INDEX subscriptions_search_tsv_idx ON subscriptions_search USING GIN (tsv);
116
+ CREATE UNIQUE INDEX subscriptions_search_subscription_id_idx ON subscriptions_search (subscription_id);
117
+
118
+ -- Create refresh function with epoch-based throttling
119
+ CREATE OR REPLACE FUNCTION refresh_subscriptions_search_view()
120
+ RETURNS TRIGGER
121
+ LANGUAGE plpgsql
122
+ AS
123
+ $$
124
+ DECLARE
125
+ should_refresh bool;
126
+ current_epoch int;
127
+ last_refresh_epoch int;
128
+ comment_sql text;
129
+ BEGIN
130
+ SELECT extract(epoch from now())::int INTO current_epoch;
131
+ SELECT coalesce(pg_catalog.obj_description('subscriptions_search'::regclass)::int, 0) INTO last_refresh_epoch;
132
+
133
+ SELECT (current_epoch - last_refresh_epoch) > 120 INTO should_refresh;
134
+
135
+ IF should_refresh THEN
136
+ REFRESH MATERIALIZED VIEW CONCURRENTLY subscriptions_search;
137
+
138
+ comment_sql := 'COMMENT ON MATERIALIZED VIEW subscriptions_search IS ' || quote_literal(current_epoch);
139
+ EXECUTE comment_sql;
140
+ END IF;
141
+ RETURN NULL;
142
+ END;
143
+ $$;
144
+
145
+ -- Create triggers
146
+ CREATE CONSTRAINT TRIGGER fi_refresh_search AFTER UPDATE ON fixed_inputs DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
147
+ CREATE CONSTRAINT TRIGGER products_refresh_search AFTER UPDATE ON products DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
148
+ CREATE CONSTRAINT TRIGGER sub_cust_desc_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_customer_descriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
149
+ CREATE CONSTRAINT TRIGGER siv_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscription_instance_values DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
150
+ CREATE CONSTRAINT TRIGGER sub_refresh_search AFTER INSERT OR UPDATE OR DELETE ON subscriptions DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION refresh_subscriptions_search_view();
151
+
152
+ -- Refresh the materialized view
153
+ REFRESH MATERIALIZED VIEW subscriptions_search;
@@ -12,10 +12,12 @@
12
12
  # limitations under the License.
13
13
 
14
14
  from datetime import datetime
15
+ from typing import Annotated
15
16
  from uuid import UUID
16
17
 
17
- from pydantic import ConfigDict
18
+ from pydantic import ConfigDict, Field
18
19
 
20
+ from orchestrator.db.models import DESCRIPTION_LENGTH
19
21
  from orchestrator.domain.lifecycle import ProductLifecycle
20
22
  from orchestrator.schemas.base import OrchestratorBaseModel
21
23
  from orchestrator.schemas.fixed_input import FixedInputSchema
@@ -44,4 +46,4 @@ class ProductSchema(ProductBaseSchema):
44
46
 
45
47
 
46
48
  class ProductPatchSchema(OrchestratorBaseModel):
47
- description: str | None = None
49
+ description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
@@ -12,10 +12,12 @@
12
12
  # limitations under the License.
13
13
 
14
14
  from datetime import datetime
15
+ from typing import Annotated
15
16
  from uuid import UUID
16
17
 
17
- from pydantic import ConfigDict
18
+ from pydantic import ConfigDict, Field
18
19
 
20
+ from orchestrator.db.models import DESCRIPTION_LENGTH
19
21
  from orchestrator.domain.lifecycle import ProductLifecycle
20
22
  from orchestrator.schemas.base import OrchestratorBaseModel
21
23
  from orchestrator.schemas.resource_type import ResourceTypeBaseSchema, ResourceTypeSchema
@@ -40,4 +42,4 @@ class ProductBlockSchema(ProductBlockBaseSchema):
40
42
 
41
43
 
42
44
  class ProductBlockPatchSchema(OrchestratorBaseModel):
43
- description: str | None = None
45
+ description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
@@ -11,10 +11,12 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
 
14
+ from typing import Annotated
14
15
  from uuid import UUID
15
16
 
16
- from pydantic import ConfigDict
17
+ from pydantic import ConfigDict, Field
17
18
 
19
+ from orchestrator.db.models import DESCRIPTION_LENGTH
18
20
  from orchestrator.schemas.base import OrchestratorBaseModel
19
21
 
20
22
 
@@ -30,4 +32,4 @@ class ResourceTypeSchema(ResourceTypeBaseSchema):
30
32
 
31
33
 
32
34
  class ResourceTypePatchSchema(OrchestratorBaseModel):
33
- description: str | None = None
35
+ description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
@@ -34,10 +34,17 @@ class ProductSchema(BaseModel):
34
34
  product_type: str
35
35
 
36
36
 
37
+ class CursorInfoSchema(BaseModel):
38
+ total_items: int | None = None
39
+ start_cursor: int | None = None
40
+ end_cursor: int | None = None
41
+
42
+
37
43
  class SearchResultsSchema(BaseModel, Generic[T]):
38
44
  data: list[T] = Field(default_factory=list)
39
45
  page_info: PageInfoSchema = Field(default_factory=PageInfoSchema)
40
46
  search_metadata: SearchMetadata | None = None
47
+ cursor: CursorInfoSchema | None = None
41
48
 
42
49
 
43
50
  class PathsResponse(BaseModel):
@@ -12,11 +12,12 @@
12
12
  # limitations under the License.
13
13
 
14
14
  from datetime import datetime
15
- from typing import Any
15
+ from typing import Annotated, Any
16
16
  from uuid import UUID
17
17
 
18
18
  from pydantic import ConfigDict, Field
19
19
 
20
+ from orchestrator.db.models import DESCRIPTION_LENGTH
20
21
  from orchestrator.schemas.base import OrchestratorBaseModel
21
22
  from orchestrator.targets import Target
22
23
 
@@ -65,4 +66,4 @@ class SubscriptionWorkflowListsSchema(OrchestratorBaseModel):
65
66
 
66
67
 
67
68
  class WorkflowPatchSchema(OrchestratorBaseModel):
68
- description: str | None = None
69
+ description: Annotated[str, Field(max_length=DESCRIPTION_LENGTH)] | None = None
@@ -12,6 +12,7 @@
12
12
  # limitations under the License.
13
13
 
14
14
  import structlog
15
+ from sqlalchemy import Select, func, select
15
16
  from sqlalchemy.orm import Session
16
17
 
17
18
  from orchestrator.search.core.embedding import QueryEmbedder
@@ -30,6 +31,7 @@ from orchestrator.search.retrieval.retrievers import (
30
31
  RrfHybridRetriever,
31
32
  SemanticRetriever,
32
33
  )
34
+ from orchestrator.search.retrieval.retrievers.structured import StructuredRetriever
33
35
 
34
36
  from .builder import build_aggregation_query, build_candidate_query, build_simple_count_query
35
37
  from .export import fetch_export_data
@@ -91,6 +93,28 @@ def _get_retriever_from_override(
91
93
  )
92
94
 
93
95
 
96
+ def _create_cursor_info(
97
+ final_stmt: Select,
98
+ db_session: Session,
99
+ cursor: PageCursor | None,
100
+ query: SelectQuery | ExportQuery,
101
+ query_embedding: list[float] | None,
102
+ candidate_query: Select,
103
+ row_count: int,
104
+ ) -> tuple[int | None, int | None, int | None]:
105
+ total_count_stmt = Retriever.route(query, None, query_embedding).apply(candidate_query) if cursor else final_stmt
106
+ total_items_or_none = db_session.scalar(select(func.count()).select_from(total_count_stmt.subquery()))
107
+ total_items = total_items_or_none or 0
108
+ count_with_cursor_or_none = (
109
+ db_session.scalar(select(func.count()).select_from(final_stmt.subquery())) if cursor else total_items
110
+ )
111
+ count_with_cursor = count_with_cursor_or_none or 0
112
+ start_cursor = total_items - count_with_cursor
113
+ row_end_cursor_count = row_count - (2 if row_count > query.limit else 1)
114
+ end_cursor = start_cursor + row_end_cursor_count
115
+ return total_items, start_cursor, end_cursor
116
+
117
+
94
118
  async def _execute_search(
95
119
  query: SelectQuery | ExportQuery,
96
120
  db_session: Session,
@@ -126,14 +150,24 @@ async def _execute_search(
126
150
  logger.debug("Using retriever", retriever_type=retriever.__class__.__name__)
127
151
 
128
152
  final_stmt = retriever.apply(candidate_query)
129
- final_stmt = final_stmt.limit(limit)
130
- logger.debug(final_stmt)
131
- result = db_session.execute(final_stmt).mappings().all()
153
+ final_stmt_with_limit = final_stmt.limit(limit)
154
+ logger.debug(final_stmt_with_limit)
155
+
156
+ result = db_session.execute(final_stmt_with_limit).mappings().all()
157
+ result_rows = list(result)
158
+ row_count = len(result_rows)
159
+
160
+ total_items: int | None = None
161
+ start_cursor: int | None = None
162
+ end_cursor: int | None = None
163
+ if isinstance(retriever, StructuredRetriever) and row_count > 0:
164
+ total_items, start_cursor, end_cursor = _create_cursor_info(
165
+ final_stmt, db_session, cursor, query, query_embedding, candidate_query, row_count
166
+ )
132
167
 
133
- response = format_search_response(result, query, retriever.metadata)
134
- # Store embedding in response for agent to save to DB
135
- response.query_embedding = query_embedding
136
- return response
168
+ return format_search_response(
169
+ result_rows, query, retriever.metadata, query_embedding, total_items, start_cursor, end_cursor
170
+ )
137
171
 
138
172
 
139
173
  async def execute_search(
@@ -66,6 +66,9 @@ class SearchResponse(BaseModel):
66
66
  metadata: SearchMetadata
67
67
  query_embedding: list[float] | None = None
68
68
  has_more: bool = False
69
+ total_items: int | None = None
70
+ start_cursor: int | None = None
71
+ end_cursor: int | None = None
69
72
 
70
73
 
71
74
  class AggregationResult(BaseModel):
@@ -221,7 +224,13 @@ def generate_highlight_indices(text: str, term: str) -> list[tuple[int, int]]:
221
224
 
222
225
 
223
226
  def format_search_response(
224
- db_rows: Sequence[RowMapping], query: "SelectQuery | ExportQuery", metadata: SearchMetadata
227
+ db_rows: Sequence[RowMapping],
228
+ query: "SelectQuery | ExportQuery",
229
+ metadata: SearchMetadata,
230
+ query_embedding: list[float] | None,
231
+ total_items: int | None,
232
+ start_cursor: int | None,
233
+ end_cursor: int | None,
225
234
  ) -> SearchResponse:
226
235
  """Format database query results into a `SearchResponse`.
227
236
 
@@ -232,6 +241,10 @@ def format_search_response(
232
241
  db_rows: The rows returned from the executed SQLAlchemy query.
233
242
  query: SelectQuery or ExportQuery with search criteria.
234
243
  metadata: Metadata about the search execution.
244
+ query_embedding: query embedding for agent to save to DB.
245
+ total_items: total items of the query (only with structured search).
246
+ start_cursor: start cursor of the db_rows (only with structured search).
247
+ end_cursor: end cursor of the db_rows (only with structured search).
235
248
 
236
249
  Returns:
237
250
  SearchResponse: A list of `SearchResult` objects containing entity IDs, scores,
@@ -280,7 +293,14 @@ def format_search_response(
280
293
  matching_field=matching_field,
281
294
  )
282
295
  )
283
- return SearchResponse(results=results, metadata=metadata)
296
+ return SearchResponse(
297
+ results=results,
298
+ metadata=metadata,
299
+ total_items=total_items,
300
+ start_cursor=start_cursor,
301
+ end_cursor=end_cursor,
302
+ query_embedding=query_embedding,
303
+ )
284
304
 
285
305
 
286
306
  def _extract_matching_field_from_filters(filters: "FilterTree") -> MatchingField | None:
@@ -30,6 +30,7 @@ from oauth2_lib.fastapi import OIDCUserModel
30
30
  from orchestrator.api.error_handling import raise_status
31
31
  from orchestrator.config.assignee import Assignee
32
32
  from orchestrator.db import EngineSettingsTable, ProcessStepTable, ProcessSubscriptionTable, ProcessTable, db
33
+ from orchestrator.db.models import FAILED_REASON_LENGTH, TRACEBACK_LENGTH
33
34
  from orchestrator.distlock import distlock_manager
34
35
  from orchestrator.schemas.engine_settings import WorkerStatus
35
36
  from orchestrator.services.input_state import store_input_state
@@ -139,8 +140,9 @@ def _update_process(process_id: UUID, step: Step, process_state: WFProcess) -> P
139
140
  # pop also removes the traceback from the dict
140
141
  traceback = step_state.pop("traceback", None)
141
142
 
142
- p.failed_reason = failed_reason
143
- p.traceback = traceback
143
+ # Truncate failed_reason (from end) and traceback (from start) to fit database constraints
144
+ p.failed_reason = failed_reason[:FAILED_REASON_LENGTH] if failed_reason else failed_reason
145
+ p.traceback = traceback[-TRACEBACK_LENGTH:] if traceback else traceback
144
146
 
145
147
  if process_state.isfailed() and p.is_task:
146
148
  # Check if we need a special failed status:
@@ -341,8 +343,11 @@ def _db_log_process_ex(process_id: UUID, ex: Exception) -> None:
341
343
  p.last_step = "Unknown"
342
344
  if p.last_status != ProcessStatus.WAITING:
343
345
  p.last_status = ProcessStatus.FAILED
344
- p.failed_reason = str(ex)
345
- p.traceback = show_ex(ex)
346
+ failed_reason = str(ex)
347
+ traceback = show_ex(ex)
348
+ # Truncate failed_reason (from end) and traceback (from start) to fit database constraints
349
+ p.failed_reason = failed_reason[:FAILED_REASON_LENGTH] if failed_reason else failed_reason
350
+ p.traceback = traceback[-TRACEBACK_LENGTH:] if traceback else traceback
346
351
  db.session.add(p)
347
352
  try:
348
353
  db.session.commit()
@@ -10,7 +10,12 @@
10
10
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
+ from typing import Annotated
14
+
15
+ from pydantic import Field
16
+
13
17
  from orchestrator.db import db
18
+ from orchestrator.db.models import NOTE_LENGTH
14
19
  from orchestrator.forms import SubmitFormPage
15
20
  from orchestrator.services import subscriptions
16
21
  from orchestrator.settings import get_authorizers
@@ -31,7 +36,7 @@ def initial_input_form(subscription_id: UUIDstr) -> FormGenerator:
31
36
  old_note = subscription.note
32
37
 
33
38
  class ModifyNoteForm(SubmitFormPage):
34
- note: LongText = old_note
39
+ note: Annotated[LongText, Field(max_length=NOTE_LENGTH)] = old_note
35
40
 
36
41
  user_input = yield ModifyNoteForm
37
42
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestrator-core
3
- Version: 4.7.1
3
+ Version: 4.7.2rc1
4
4
  Summary: This is the orchestrator workflow engine.
5
5
  Author-email: SURF <automation-beheer@surf.nl>
6
6
  Requires-Python: >=3.11,<3.15
@@ -31,7 +31,7 @@ Classifier: Topic :: Software Development :: Libraries
31
31
  Classifier: Topic :: Software Development
32
32
  Classifier: Typing :: Typed
33
33
  License-File: LICENSE
34
- Requires-Dist: alembic==1.18.1
34
+ Requires-Dist: alembic==1.18.3
35
35
  Requires-Dist: anyio>=3.7.0
36
36
  Requires-Dist: apscheduler>=3.11.0
37
37
  Requires-Dist: click==8.*
@@ -44,7 +44,7 @@ Requires-Dist: jinja2==3.1.6
44
44
  Requires-Dist: more-itertools~=10.8.0
45
45
  Requires-Dist: nwa-stdlib~=1.11.0
46
46
  Requires-Dist: oauth2-lib>=2.5.0
47
- Requires-Dist: orjson==3.11.5
47
+ Requires-Dist: orjson==3.11.7
48
48
  Requires-Dist: pgvector>=0.4.1
49
49
  Requires-Dist: prometheus-client==0.24.1
50
50
  Requires-Dist: psycopg2-binary==2.9.11
@@ -57,7 +57,7 @@ Requires-Dist: pytz==2025.2
57
57
  Requires-Dist: redis==7.1.0
58
58
  Requires-Dist: semver==3.0.4
59
59
  Requires-Dist: sentry-sdk[fastapi]>=2.29.1
60
- Requires-Dist: sqlalchemy==2.0.45
60
+ Requires-Dist: sqlalchemy==2.0.46
61
61
  Requires-Dist: sqlalchemy-utils==0.42.1
62
62
  Requires-Dist: strawberry-graphql>=0.281.0,<0.285.0
63
63
  Requires-Dist: structlog>=25.4.0
@@ -1,4 +1,4 @@
1
- orchestrator/__init__.py,sha256=ZUMqFoYxZjh4nowQu2_YP5vbtKfQeVG2YNxYm78KPYM,1454
1
+ orchestrator/__init__.py,sha256=p4XGF-AzKy7JAUS3i9s8xJYE20rWVg8KZxv3Cu3VCYY,1457
2
2
  orchestrator/agentic_app.py,sha256=ouiyyZiS4uS6Lox2DtbGGRnb2njJBMSHpSAGe-T5rX0,3028
3
3
  orchestrator/app.py,sha256=5ITGSN_KeRi2qTvfwBXhjOGNyWNy-rdtzfOLEk76ZtY,14661
4
4
  orchestrator/exception_handlers.py,sha256=UsW3dw8q0QQlNLcV359bIotah8DYjMsj2Ts1LfX4ClY,1268
@@ -25,7 +25,7 @@ orchestrator/api/api_v1/endpoints/product_blocks.py,sha256=kZ6ywIOsS_S2qGq7RvZ4K
25
25
  orchestrator/api/api_v1/endpoints/products.py,sha256=BfFtwu9dZXEQbtKxYj9icc73GKGvAGMR5ytyf41nQlQ,3081
26
26
  orchestrator/api/api_v1/endpoints/resource_types.py,sha256=gGyuaDyOD0TAVoeFGaGmjDGnQ8eQQArOxKrrk4MaDzA,2145
27
27
  orchestrator/api/api_v1/endpoints/schedules.py,sha256=eTG_4CQkiIi2akJUN4xDGuU_OvF6Ml6uye5MmQ_WJbc,1731
28
- orchestrator/api/api_v1/endpoints/search.py,sha256=R_OzfJfquoaTeGBwXTbomh16aYy0ael6_Xn3WkKfyjg,8575
28
+ orchestrator/api/api_v1/endpoints/search.py,sha256=363lacZRRexlfLbL4dVRhgT1ir1u43dEKYt4zHuWxH0,8986
29
29
  orchestrator/api/api_v1/endpoints/settings.py,sha256=5s-k169podZjgGHUbVDmSQwpY_3Cs_Bbf2PPtZIkBcw,6184
30
30
  orchestrator/api/api_v1/endpoints/subscription_customer_descriptions.py,sha256=1_6LtgQleoq3M6z_W-Qz__Bj3OFUweoPrUqHMwSH6AM,3288
31
31
  orchestrator/api/api_v1/endpoints/subscriptions.py,sha256=7KaodccUiMkcVnrFnK2azp_V_-hGudcIyhov5WwVGQY,9810
@@ -117,7 +117,7 @@ orchestrator/db/database.py,sha256=MU_w_e95ho2dVb2JDnt_KFYholx___XDkiQXbc8wCkI,1
117
117
  orchestrator/db/helpers.py,sha256=L8kEdnSSNGnUpZhdeGx2arCodakWN8vSpKdfjoLuHdY,831
118
118
  orchestrator/db/listeners.py,sha256=UBPYcH0FE3a7aZQu_D0O_JMXpXIRYXC0gjSAvlv5GZo,1142
119
119
  orchestrator/db/loaders.py,sha256=ez6JzQ3IKVkC_oLAkVlIIiI8Do7hXbdcPKCvUSLxRog,7962
120
- orchestrator/db/models.py,sha256=qsXOonEDaxn_UXfuXLQfdnm1PV6a-JZVpXa46vd--Es,32201
120
+ orchestrator/db/models.py,sha256=oc0MAwWzQn3KpKWMEHR_oxmQ09NW1g6vzU4Pqv_Brk0,33551
121
121
  orchestrator/db/filters/__init__.py,sha256=RUj6P0XxEBhYj0SN5wH5-Vf_Wt_ilZR_n9DSar5m9oM,371
122
122
  orchestrator/db/filters/filters.py,sha256=55RtpQwM2rhrk4A6CCSeSXoo-BT9GnQoNTryA8CtLEg,5020
123
123
  orchestrator/db/filters/process.py,sha256=xvGhyfo_MZ1xhLvFC6yULjcT4mJk0fKc1glJIYgsWLE,4018
@@ -260,6 +260,9 @@ orchestrator/migrations/versions/schema/2025-07-04_4b58e336d1bf_deprecating_work
260
260
  orchestrator/migrations/versions/schema/2025-07-28_850dccac3b02_update_description_of_resume_workflows_.py,sha256=R6Qoga83DJ1IL0WYPu0u5u2ZvAmqGlDmUMv_KtJyOhQ,812
261
261
  orchestrator/migrations/versions/schema/2025-11-18_961eddbd4c13_create_linker_table_workflow_apscheduler.py,sha256=Vy2qA8wb_lQWExhF0PX_IFwCr_vafe9uaT1pXvCwbGI,3227
262
262
  orchestrator/migrations/versions/schema/2025-12-10_9736496e3eba_set_is_task_true_on_certain_tasks.py,sha256=2DOERJ7QF83o-goxJPtz0FUC3xZAt5ms8miadFGVFcw,1007
263
+ orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to_.py,sha256=kIOBK9ft3UA5BTbNROYKD5usXBJT7FqgNMcQLEZOqAw,813
264
+ orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to__downgrade.sql,sha256=xTEZEwEXIUVlNyR7HeMKrXKgcYryOI_CGRqPbCGXRYo,7683
265
+ orchestrator/migrations/versions/schema/2026-01-12_d69e10434a04_convert_unlimited_text_fields_to__upgrade.sql,sha256=RvUmpkZohnxsMXmBVBz0oPALEYkEEMYxRP-_MrrNbEk,8387
263
266
  orchestrator/schedules/__init__.py,sha256=i8sT88A3v_5KIfwbKZxe3rS2rMakOuqfAis0DRmBleU,1017
264
267
  orchestrator/schedules/scheduler.py,sha256=8o7DoVs9Q1Q231FVMpv3tXtKbaydeNkYQ1h6kl7U1X4,7198
265
268
  orchestrator/schedules/scheduling.py,sha256=1lSeAhKRGhZNOtFiB-FPMeo3bEIDpt9OdJKBkk7QknI,2914
@@ -271,15 +274,15 @@ orchestrator/schemas/engine_settings.py,sha256=LF8al7tJssiilb5A4emPtUYo0tVDSaT1L
271
274
  orchestrator/schemas/fixed_input.py,sha256=Rth3hT5K7zYuQr1bUY_NJRzb03xEZuT1p6EvYXVNE54,1214
272
275
  orchestrator/schemas/problem_detail.py,sha256=DxiUhWv6EVXLZgdKFv0EYVnCgtkDj7xteDCR0q2f5yw,802
273
276
  orchestrator/schemas/process.py,sha256=UACBNt-4g4v9Y528u-gZ-Wk7YxwJHhnI4cEu5CtQm2w,2541
274
- orchestrator/schemas/product.py,sha256=MhMCh058ZuS2RJq-wSmxIPUNlhQexxXIx3DSz2OmOh4,1570
275
- orchestrator/schemas/product_block.py,sha256=kCqvm6qadHpegMr9aWI_fYX-T7mS-5S-ldPxnGQZg7M,1519
276
- orchestrator/schemas/resource_type.py,sha256=VDju4XywcDDLxdpbWU62RTvR9QF8x_GRrpTlN_NE8uI,1064
277
+ orchestrator/schemas/product.py,sha256=oovz_HAY2asih_sFiwzMyRmdSmJMymFIRPSPx3L-66I,1709
278
+ orchestrator/schemas/product_block.py,sha256=pIRQUxI6Rm_T6o4MiY6bzrLVKYZSWCR-vUQsjMa52Bg,1658
279
+ orchestrator/schemas/resource_type.py,sha256=AhYKUe8NVWgQx4DOCCgQ4CFcJA0pAX9EE5OCLQYq0z4,1203
277
280
  orchestrator/schemas/schedules.py,sha256=Gb427IGR5mPTjKN8STwUhAWCJMCywJkrS8OetiiHTKY,2844
278
- orchestrator/schemas/search.py,sha256=d_Vs1qU9Z5zuXN4pDk6jrVwiUXRKZ93U-tHW5Zfrw-w,1546
281
+ orchestrator/schemas/search.py,sha256=UJ4R8zZ4rnXybOCi7KPDzA6TYaCTVqmHeDxrKJxny5U,1731
279
282
  orchestrator/schemas/search_requests.py,sha256=2gb1mbzzMmSbMTtLmItrTSPWRXNKwdgoPIEiNFhTFjA,2144
280
283
  orchestrator/schemas/subscription.py,sha256=-jXyHZIed9Xlia18ksSDyenblNN6Q2yM2FlGELyJ458,3423
281
284
  orchestrator/schemas/subscription_descriptions.py,sha256=Ft_jw1U0bf9Z0U8O4OWfLlcl0mXCVT_qYVagBP3GbIQ,1262
282
- orchestrator/schemas/workflow.py,sha256=StVoRGyNT2iIeq3z8BIlTPt0bcafzbeYxXRrCucR6LU,2146
285
+ orchestrator/schemas/workflow.py,sha256=d0zJIf6XM7ny1-y-7uDYF2pJFbWPCRXn-PC04PUH9i0,2260
283
286
  orchestrator/search/__init__.py,sha256=2uhTQexKx-cdBP1retV3CYSNCs02s8WL3fhGvupRGZk,571
284
287
  orchestrator/search/llm_migration.py,sha256=afDqdo5t-L6gLduKSSVKuAegznPdIsQn4YIoAAFraC8,6767
285
288
  orchestrator/search/agent/__init__.py,sha256=_O4DN0MSTUtr4olhyE0-2hsb7x3f_KURMCYjg8jV4QA,756
@@ -312,12 +315,12 @@ orchestrator/search/indexing/tasks.py,sha256=0p68RNwJnHSGZQjfdpyFsS2Ma5Gr2PpZROZ
312
315
  orchestrator/search/indexing/traverse.py,sha256=JLut9t4LoPCWhJ_63VgYhRKfjwyxRv-mTbQLC8mA_mU,15158
313
316
  orchestrator/search/query/__init__.py,sha256=nCjvK_n2WQdV_ACrncFXEfnvLcHtuI__J7KLlFIaQvo,2437
314
317
  orchestrator/search/query/builder.py,sha256=EfDSSOQKUBNtUESDBsKaPY6hZ_iDXAwc3qcNR4AGAEg,13261
315
- orchestrator/search/query/engine.py,sha256=mlUrK_FKfytdYgsvhaZaiHSBvlqUvFERS5VAfzFHuNM,7920
318
+ orchestrator/search/query/engine.py,sha256=M6UFHjU5ovLiPPvhXGgM7xCCCZZyD6Hgh13niPgjj6U,9427
316
319
  orchestrator/search/query/exceptions.py,sha256=DrkNzXVbQAOi28FTHKimf_eTrXmhYwXrH986QhfQLPU,4941
317
320
  orchestrator/search/query/export.py,sha256=_0ncVpTqN6AoQfW3WX0fWnDQX3hBz6ZGC31Beu4PVwQ,6678
318
321
  orchestrator/search/query/mixins.py,sha256=8zvrQMlIkWt3q0BFfekm9ugVmuu85GaKQBEgJxUQmj4,5178
319
322
  orchestrator/search/query/queries.py,sha256=0jF97cU2Z98-oWm1Iyqf3xIgrmc7FcWAPTb51tUG4MA,4506
320
- orchestrator/search/query/results.py,sha256=5OgAs39oncDIBdpB3NJltPr-UvLvLlxTWw9sn-lyfQA,10989
323
+ orchestrator/search/query/results.py,sha256=CtCF9iJV0_Z0PSqiD8XhfVruqVKaBFNuRNmpjKVZoKQ,11695
321
324
  orchestrator/search/query/state.py,sha256=fMSBJs39kZTkpDE2T4h4x0x-51GqUvzAuePg2YUbO6I,3220
322
325
  orchestrator/search/query/validation.py,sha256=Pprv40yvpynL1-MCFE1YuouguYW6lfh1PZKsVei2i6w,9622
323
326
  orchestrator/search/retrieval/__init__.py,sha256=q5G0z3nKjIHKFs1PkEG3nvTUy3Wp4kCyBtCbqUITj3A,579
@@ -333,7 +336,7 @@ orchestrator/services/__init__.py,sha256=GyHNfEFCGKQwRiN6rQmvSRH2iYX7npjMZn97n8X
333
336
  orchestrator/services/fixed_inputs.py,sha256=kyz7s2HLzyDulvcq-ZqefTw1om86COvyvTjz0_5CmgI,876
334
337
  orchestrator/services/input_state.py,sha256=6BZOpb3cHpO18K-XG-3QUIV9pIM25_ufdODrp5CmXG4,2390
335
338
  orchestrator/services/process_broadcast_thread.py,sha256=D44YbjF8mRqGuznkRUV4SoRn1J0lfy_x1H508GnSVlU,4649
336
- orchestrator/services/processes.py,sha256=vMk30ImSE_0NXM1ffiBvXvaenAeqEYgQbbu_m-4ruGk,30350
339
+ orchestrator/services/processes.py,sha256=FGCzlj-B0PYeYIbjwIwCEPNTKZUnoOAeEQuXq-dQ5Ds,30892
337
340
  orchestrator/services/products.py,sha256=BP4KyE8zO-8z7Trrs5T6zKBOw53S9BfBJnHWI3p6u5Y,1943
338
341
  orchestrator/services/resource_types.py,sha256=_QBy_JOW_X3aSTqH0CuLrq4zBJL0p7Q-UDJUcuK2_qc,884
339
342
  orchestrator/services/settings.py,sha256=HEWfFulgoEDwgfxGEO__QTr5fDiwNBEj1UhAeTAdbLQ,3159
@@ -372,7 +375,7 @@ orchestrator/websocket/websocket_manager.py,sha256=hwlG9FDXcNU42jDNNsPMQLIyrvEpG
372
375
  orchestrator/websocket/managers/broadcast_websocket_manager.py,sha256=fwoSgTjkHJ2GmsLTU9dqQpAA9i8b1McPu7gLNzxtfG4,5401
373
376
  orchestrator/websocket/managers/memory_websocket_manager.py,sha256=lF5EEx1iFMCGEkTbItTDr88NENMSaSeG1QrJ7teoPkY,3324
374
377
  orchestrator/workflows/__init__.py,sha256=FbwcAYJh8oSi0QFjXXXomdl9c8whCa_qSt_vPXcwasE,4216
375
- orchestrator/workflows/modify_note.py,sha256=WFK3rA3Cmpk2_kOUP3xDfe9OI5LuQGv09tRAoTVKaR4,2360
378
+ orchestrator/workflows/modify_note.py,sha256=dSNXsthIVqv0BwbYO33ZORCpbDLoNFNqnSr3NNBKMmI,2507
376
379
  orchestrator/workflows/removed_workflow.py,sha256=fwi1-aC1KQvb08hq8St-_lWOLM_tjTcQMLJ_Fjdn2M8,1111
377
380
  orchestrator/workflows/steps.py,sha256=VVLRK9_7KzrBlnK7L8eSmRMNVOO7VJBh5OSjHQHM9fU,7019
378
381
  orchestrator/workflows/utils.py,sha256=VUCDoIl5XAKtIeAJpVpyW2pCIg3PoVWfwGn28BYlYhA,15424
@@ -383,7 +386,7 @@ orchestrator/workflows/tasks/validate_product_type.py,sha256=KoDMqROGVQ0ZPu69jMF
383
386
  orchestrator/workflows/tasks/validate_products.py,sha256=lCAXmCVhohgrdgJn7-d7fIxPj4MVOX0J8KezcvwIK3k,8716
384
387
  orchestrator/workflows/tasks/validate_subscriptions.py,sha256=CFKf3igyrqGm-zzSMD0wbNLgJwKz8quNNgnNWDqgpI0,2387
385
388
  orchestrator/workflows/translations/en-GB.json,sha256=ObBlH9XILJ9uNaGcJexi3IB0e6P8CKFKRgu29luIEM8,973
386
- orchestrator_core-4.7.1.dist-info/licenses/LICENSE,sha256=b-aA5OZQuuBATmLKo_mln8CQrDPPhg3ghLzjPjLn4Tg,11409
387
- orchestrator_core-4.7.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
388
- orchestrator_core-4.7.1.dist-info/METADATA,sha256=2plaKnYq_wj2iBQX1jWqk6b773iTh0htol6M65lVsW4,6418
389
- orchestrator_core-4.7.1.dist-info/RECORD,,
389
+ orchestrator_core-4.7.2rc1.dist-info/licenses/LICENSE,sha256=b-aA5OZQuuBATmLKo_mln8CQrDPPhg3ghLzjPjLn4Tg,11409
390
+ orchestrator_core-4.7.2rc1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
391
+ orchestrator_core-4.7.2rc1.dist-info/METADATA,sha256=sNJgIhSwYBLt30MlpBniJDGtuQ-VY9bLIGtvJ2hUYrA,6421
392
+ orchestrator_core-4.7.2rc1.dist-info/RECORD,,