Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/poly/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,12 @@ def _update_multi_resource_yaml_resources(
for file_path in self._sort_paths_for_reverse_deletion(deleted_paths, resource_type):
resource_type.delete_resource(file_path, save_to_cache=True)

# The deletions above only reached the cache. Flush them, then clear: a cached
# entry carries the pre-write mtime, so anything left behind makes every later
# read in this process see a file state that is not on disk.
MultiResourceYamlResource.write_cache_to_file()
MultiResourceYamlResource._file_cache.clear()

return files_with_conflicts, progress_offset

def _update_pulled_resources(
Expand Down
7 changes: 7 additions & 0 deletions src/poly/resources/agent_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Copyright PolyAI Limited
"""

import logging
import os
from dataclasses import dataclass
from functools import cached_property
Expand All @@ -21,6 +22,8 @@
register_resource,
)

logger = logging.getLogger(__name__)

ALLOWED_BEHAVIOUR_REFERENCES = [
"global_functions",
"sms",
Expand Down Expand Up @@ -199,6 +202,10 @@ def validate(self, resource_mappings: list[ResourceMapping] = None, **kwargs) ->
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "SettingsRules"]:
"""Parse rules settings from a projection dict."""
if "agentSettings" not in projection:
logger.debug("No read access to the agent rules - it will not be pulled.")
return {}

agent_settings = projection.get("agentSettings", {})
rules = agent_settings.get("rules", None)
if not rules:
Expand Down
4 changes: 4 additions & 0 deletions src/poly/resources/api_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ class ApiIntegration(MultiResourceYamlResource):
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "ApiIntegration"]:
"""Parse API integrations from a projection dict."""
if "apiIntegrations" not in projection:
logger.debug("No read access to API integrations - it will not be pulled.")
return {}

api_integrations = {}
for integration_id, integration_data in (
projection.get("apiIntegrations", {})
Expand Down
4 changes: 4 additions & 0 deletions src/poly/resources/asr_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def __init__(
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "AsrSettings"]:
"""Parse ASR settings from a projection dict."""
if "channels" not in projection:
logger.debug("No read access to ASR settings - it will not be pulled.")
return {}

asr_settings_data = projection.get("channels", {}).get("voice", {}).get("asrSettings", {})
if not asr_settings_data:
return {}
Expand Down
23 changes: 23 additions & 0 deletions src/poly/resources/channel_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Copyright PolyAI Limited
"""

import logging
import os
from dataclasses import dataclass, field
from functools import cached_property
Expand All @@ -26,6 +27,8 @@
register_resource,
)

logger = logging.getLogger(__name__)


def _config_path(channel: str) -> str:
"""Return config path for a channel (e.g. voice/configuration.yaml)."""
Expand Down Expand Up @@ -122,6 +125,10 @@ def update_command_type(self) -> str:
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "VoiceDisclaimerMessage"]:
"""Parse voice disclaimer from a projection dict."""
if "channels" not in projection:
logger.debug("No read access to the voice disclaimer - it will not be pulled.")
return {}

voice_settings = projection.get("channels", {}).get("voice", {})
voice_disclaimer = voice_settings.get("disclaimer", None)
if not voice_disclaimer:
Expand Down Expand Up @@ -268,6 +275,10 @@ class VoiceGreeting(ChannelGreeting):
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "VoiceGreeting"]:
"""Parse voice greeting from a projection dict."""
if "channels" not in projection:
logger.debug("No read access to the voice greeting - it will not be pulled.")
return {}

voice_config = projection.get("channels", {}).get("voice", {}).get("config", {}) or {}
voice_greeting = voice_config.get("greeting", None)
if not voice_greeting:
Expand All @@ -293,6 +304,10 @@ class ChatGreeting(ChannelGreeting):
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "ChatGreeting"]:
"""Parse chat greeting from a projection dict."""
if "channels" not in projection:
logger.debug("No read access to the chat greeting - it will not be pulled.")
return {}

web_chat_settings = projection.get("channels", {}).get("webChat", {})
if not web_chat_settings.get("status", False):
return {}
Expand Down Expand Up @@ -401,6 +416,10 @@ class VoiceStylePrompt(ChannelStylePrompt):
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "VoiceStylePrompt"]:
"""Parse voice style prompt from a projection dict."""
if "channels" not in projection:
logger.debug("No read access to the voice style prompt - it will not be pulled.")
return {}

voice_config = projection.get("channels", {}).get("voice", {}).get("config", {}) or {}
voice_style_prompt = voice_config.get("stylePrompt", None)
if not voice_style_prompt:
Expand All @@ -425,6 +444,10 @@ class ChatStylePrompt(ChannelStylePrompt):
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "ChatStylePrompt"]:
"""Parse chat style prompt from a projection dict."""
if "channels" not in projection:
logger.debug("No read access to the chat style prompt - it will not be pulled.")
return {}

web_chat_settings = projection.get("channels", {}).get("webChat", {})
if not web_chat_settings.get("status", False):
return {}
Expand Down
19 changes: 13 additions & 6 deletions src/poly/resources/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Copyright PolyAI Limited
"""

import logging
import os
from dataclasses import dataclass
from functools import cached_property
Expand All @@ -14,6 +15,8 @@
)
from poly.resources.resource import Resource, register_resource

logger = logging.getLogger(__name__)

PLATFORM_CONTEXT_FILE = "CONTEXT.MD"


Expand Down Expand Up @@ -127,13 +130,17 @@ def discover_resources(base_path: str) -> list[str]:
@classmethod
def from_projection(cls, projection: dict) -> dict[str, "Document"]:
documents = {}
for document_id, document_data in (
projection.get("documents", {}).get("documents", {}).get("entities", {}).items()
documents_projection = (
projection.get("documents", {}).get("documents", {}).get("entities", {})
)
# The projection carries the proto field name, "content".
if "documents" not in projection or any(
"content" not in doc for doc in documents_projection.values()
):
# The projection carries the proto field name, "content". An entity without it is
# one the current user lacks read permission for, so skip it.
if "content" not in document_data:
continue
logger.debug("No read access to context documents - they will not be pulled.")
return {}

for document_id, document_data in documents_projection.items():
path = document_data.get("path", "") or ""
name = path.removesuffix(".md").removesuffix(".MD")
documents[document_id] = Document(
Expand Down
12 changes: 10 additions & 2 deletions src/poly/resources/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Copyright PolyAI Limited
"""

import logging
import os
from dataclasses import dataclass, field
from enum import Enum
Expand All @@ -27,6 +28,8 @@
)
from poly.resources.resource import MultiResourceYamlResource, ResourceMapping, register_resource

logger = logging.getLogger(__name__)


class EntityType(str, Enum):
"""Enum representing the type of an Entity"""
Expand Down Expand Up @@ -108,9 +111,14 @@ def __init__(
def from_projection(cls, projection: dict) -> dict[str, "Entity"]:
"""Parse entities from a projection dict."""
entities = {}
for entity_id, entity_data in (
projection.get("entities", {}).get("entities", {}).get("entities", {}).items()
entities_projection = projection.get("entities", {}).get("entities", {}).get("entities", {})
if "entities" not in projection or any(
"type" not in entity for entity in entities_projection.values()
):
logger.debug("No read access to entities - they will not be pulled.")
return {}

for entity_id, entity_data in entities_projection.items():
entities[entity_id] = cls(
resource_id=entity_id,
name=entity_data["name"],
Expand Down
11 changes: 11 additions & 0 deletions src/poly/resources/experimental_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
"""

import json
import logging
import os
from dataclasses import dataclass, field

import poly.resources.resource_utils as utils
from poly.handlers.protobuf.experimental_config_pb2 import ExperimentalConfig_UpdateConfig
from poly.resources.resource import Resource, register_resource

logger = logging.getLogger(__name__)


@register_resource("experimental_config")
@dataclass
Expand All @@ -27,6 +30,14 @@ def from_projection(cls, projection: dict) -> dict[str, "ExperimentalConfig"]:
.get("experimentalConfigs", {})
.get("entities", {})
)
# "features" is optional in the API schema, so it can't distinguish an
# auth-filtered config from one with no features set. "active" always is.
if "experimentalConfig" not in projection or any(
"active" not in config for config in experimental_configs.values()
):
logger.debug("No read access to experimental config - it will not be pulled.")
return {}

config_id, config_data = (
next(iter(experimental_configs.items()), ("default", {}))
if experimental_configs
Expand Down
24 changes: 24 additions & 0 deletions src/poly/resources/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Copyright PolyAI Limited
"""

import logging
import math
import os
import re
Expand Down Expand Up @@ -60,6 +61,9 @@
register_resource,
)

logger = logging.getLogger(__name__)


FUNCTION_REGEX = re.compile(r"{{f[nt]:([\w-]+)}}")
# Flow step names: alphanumeric, extended Latin (C0–024F, 1E00–1EFF), and _ &,/.-
FLOW_STEP_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9\u00C0-\u024F\u1E00-\u1EFF_ &,/.\-]+$")
Expand Down Expand Up @@ -109,6 +113,10 @@ def from_projection(cls, projection: dict) -> dict[str, "FlowConfig"]:
"""Parse flow configs from a projection dict."""
configs = {}
flows = projection.get("flows", {}).get("flows", {}).get("entities", {})
if "flows" not in projection or any("startStepId" not in flow for flow in flows.values()):
logger.debug("No read access to flows - they will not be pulled.")
return {}

for flow_id, flow_data in flows.items():
configs[flow_id] = cls(
resource_id=flow_id,
Expand Down Expand Up @@ -368,6 +376,14 @@ def from_projection(cls, projection: dict) -> dict[str, "FlowStep"]:
"""Parse flow steps (non-function) from a projection dict."""
steps = {}
flows = projection.get("flows", {}).get("flows", {}).get("entities", {})
if "flows" not in projection or any(
"type" not in step
for flow_data in flows.values()
for step in flow_data.get("steps", {}).get("entities", {}).values()
):
logger.debug("No read access to flow steps - they will not be pulled.")
return {}

for flow_id, flow_data in flows.items():
for step_id, step in flow_data.get("steps", {}).get("entities", {}).items():
if step.get("type") == "function_step":
Expand Down Expand Up @@ -1732,6 +1748,14 @@ def from_projection(cls, projection: dict) -> dict[str, "FunctionStep"]:
"""Parse function steps from a projection dict."""
func_steps = {}
flows = projection.get("flows", {}).get("flows", {}).get("entities", {})
if "flows" not in projection or any(
"type" not in step
for flow_data in flows.values()
for step in flow_data.get("steps", {}).get("entities", {}).values()
):
logger.debug("No read access to flow steps - they will not be pulled.")
return {}

for flow_id, flow_data in flows.items():
for step_id, step in flow_data.get("steps", {}).get("entities", {}).items():
if step.get("type") != "function_step":
Expand Down
27 changes: 25 additions & 2 deletions src/poly/resources/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,17 @@ def from_projection(cls, projection: dict) -> dict[str, "Function"]:
"""Parse functions from a projection dict."""
functions = {}

# Functions are drawn from three projection slices gated on two different
# permissions ("functions" for special and global, "jupiter_flows" for
# transition), so each slice is checked for read access independently -
# losing one must not hide the others.
special_functions = projection.get("specialFunctions", {})
if "specialFunctions" not in projection or any(
"code" not in func for func in special_functions.values()
):
logger.debug("No read access to start/end functions - they will not be pulled.")
special_functions = {}

for func_type_key, func in special_functions.items():
if func.get("archived", False):
continue
Expand Down Expand Up @@ -316,6 +326,14 @@ def from_projection(cls, projection: dict) -> dict[str, "Function"]:
)

flows = projection.get("flows", {}).get("flows", {}).get("entities", {})
if "flows" not in projection or any(
"code" not in func
for flow_data in flows.values()
for func in flow_data.get("transitionFunctions", {}).get("entities", {}).values()
):
logger.debug("No read access to transition functions - they will not be pulled.")
flows = {}

for flow_id, flow_data in flows.items():
for func_id, func in (
flow_data.get("transitionFunctions", {}).get("entities", {}).items()
Expand Down Expand Up @@ -345,9 +363,14 @@ def from_projection(cls, projection: dict) -> dict[str, "Function"]:
function_type=FunctionType.TRANSITION,
)

for func_id, func in (
projection.get("functions", {}).get("functions", {}).get("entities", {}).items()
global_functions = projection.get("functions", {}).get("functions", {}).get("entities", {})
if "functions" not in projection or any(
"code" not in func for func in global_functions.values()
):
logger.debug("No read access to functions - they will not be pulled.")
global_functions = {}

for func_id, func in global_functions.items():
if func.get("archived", False):
continue

Expand Down
11 changes: 11 additions & 0 deletions src/poly/resources/handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Copyright PolyAI Limited
"""

import logging
import os
from dataclasses import dataclass
from typing import ClassVar
Expand All @@ -24,6 +25,8 @@
register_resource,
)

logger = logging.getLogger(__name__)

VALID_SIP_METHODS = ("invite", "refer", "bye")
VALID_ENCRYPTION = ("TLS/SRTP", "UDP/RTP")

Expand Down Expand Up @@ -107,6 +110,14 @@ def from_projection(cls, projection: dict) -> dict[str, "Handoff"]:
"""Parse handoffs from a projection dict."""
handoffs_projection = projection.get("handoff", {}).get("handoffs", {}).get("entities", {})
handoffs = {}
# Read access is checked before "active": an auth-filtered handoff has no
# "active" field, and must not be mistaken for a deactivated one.
if "handoff" not in projection or any(
"active" not in handoff for handoff in handoffs_projection.values()
):
logger.debug("No read access to handoffs - they will not be pulled.")
return {}

for handoff_id, handoff_data in handoffs_projection.items():
if not handoff_data.get("active", False):
continue
Expand Down
Loading
Loading