pytrilogy 0.3.138__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
Files changed (182) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-311-x86_64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.138.dist-info/METADATA +525 -0
  5. pytrilogy-0.3.138.dist-info/RECORD +182 -0
  6. pytrilogy-0.3.138.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.138.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.138.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +9 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +87 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +143 -0
  26. trilogy/constants.py +113 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +443 -0
  31. trilogy/core/env_processor.py +120 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1227 -0
  36. trilogy/core/graph_models.py +139 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2672 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +494 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +748 -0
  45. trilogy/core/models/execute.py +1177 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +517 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +268 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +205 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +653 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +748 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +519 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +596 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +106 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1359 -0
  112. trilogy/dialect/bigquery.py +256 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +144 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +177 -0
  117. trilogy/dialect/enums.py +147 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +91 -0
  121. trilogy/dialect/presto.py +104 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +90 -0
  124. trilogy/dialect/sql_server.py +92 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/config.py +75 -0
  127. trilogy/executor.py +568 -0
  128. trilogy/hooks/__init__.py +4 -0
  129. trilogy/hooks/base_hook.py +40 -0
  130. trilogy/hooks/graph_hook.py +139 -0
  131. trilogy/hooks/query_debugger.py +166 -0
  132. trilogy/metadata/__init__.py +0 -0
  133. trilogy/parser.py +10 -0
  134. trilogy/parsing/README.md +21 -0
  135. trilogy/parsing/__init__.py +0 -0
  136. trilogy/parsing/common.py +1069 -0
  137. trilogy/parsing/config.py +5 -0
  138. trilogy/parsing/exceptions.py +8 -0
  139. trilogy/parsing/helpers.py +1 -0
  140. trilogy/parsing/parse_engine.py +2813 -0
  141. trilogy/parsing/render.py +750 -0
  142. trilogy/parsing/trilogy.lark +540 -0
  143. trilogy/py.typed +0 -0
  144. trilogy/render.py +42 -0
  145. trilogy/scripts/README.md +7 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/dependency/Cargo.lock +617 -0
  148. trilogy/scripts/dependency/Cargo.toml +39 -0
  149. trilogy/scripts/dependency/README.md +131 -0
  150. trilogy/scripts/dependency/build.sh +25 -0
  151. trilogy/scripts/dependency/src/directory_resolver.rs +162 -0
  152. trilogy/scripts/dependency/src/lib.rs +16 -0
  153. trilogy/scripts/dependency/src/main.rs +770 -0
  154. trilogy/scripts/dependency/src/parser.rs +435 -0
  155. trilogy/scripts/dependency/src/preql.pest +208 -0
  156. trilogy/scripts/dependency/src/python_bindings.rs +289 -0
  157. trilogy/scripts/dependency/src/resolver.rs +716 -0
  158. trilogy/scripts/dependency/tests/base.preql +3 -0
  159. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  160. trilogy/scripts/dependency/tests/customer.preql +6 -0
  161. trilogy/scripts/dependency/tests/main.preql +9 -0
  162. trilogy/scripts/dependency/tests/orders.preql +7 -0
  163. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  164. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  165. trilogy/scripts/dependency.py +323 -0
  166. trilogy/scripts/display.py +460 -0
  167. trilogy/scripts/environment.py +46 -0
  168. trilogy/scripts/parallel_execution.py +483 -0
  169. trilogy/scripts/single_execution.py +131 -0
  170. trilogy/scripts/trilogy.py +772 -0
  171. trilogy/std/__init__.py +0 -0
  172. trilogy/std/color.preql +3 -0
  173. trilogy/std/date.preql +13 -0
  174. trilogy/std/display.preql +18 -0
  175. trilogy/std/geography.preql +22 -0
  176. trilogy/std/metric.preql +15 -0
  177. trilogy/std/money.preql +67 -0
  178. trilogy/std/net.preql +14 -0
  179. trilogy/std/ranking.preql +7 -0
  180. trilogy/std/report.preql +5 -0
  181. trilogy/std/semantic.preql +6 -0
  182. trilogy/utility.py +34 -0
@@ -0,0 +1,106 @@
1
+ from os import environ
2
+ from typing import List, Optional
3
+
4
+ from trilogy.ai.enums import Provider
5
+ from trilogy.ai.models import LLMMessage, LLMResponse, UsageDict
6
+ from trilogy.constants import logger
7
+
8
+ from .base import RETRYABLE_CODES, LLMProvider, LLMRequestOptions
9
+ from .utils import RetryOptions, fetch_with_retry
10
+
11
+ DEFAULT_MAX_TOKENS = 10000
12
+
13
+
14
+ class AnthropicProvider(LLMProvider):
15
+ def __init__(
16
+ self,
17
+ name: str,
18
+ model: str,
19
+ api_key: str | None = None,
20
+ retry_options: Optional[RetryOptions] = None,
21
+ ):
22
+ api_key = api_key or environ.get("ANTHROPIC_API_KEY")
23
+ if not api_key:
24
+ raise ValueError(
25
+ "API key argument or environment variable ANTHROPIC_API_KEY is required"
26
+ )
27
+ super().__init__(name, api_key, model, Provider.ANTHROPIC)
28
+ self.base_completion_url = "https://api.anthropic.com/v1/messages"
29
+ self.base_model_url = "https://api.anthropic.com/v1/models"
30
+ self.models: List[str] = []
31
+ self.type = Provider.ANTHROPIC
32
+ self.retry_options = retry_options or RetryOptions(
33
+ max_retries=5,
34
+ initial_delay_ms=5000,
35
+ retry_status_codes=RETRYABLE_CODES,
36
+ on_retry=lambda attempt, delay_ms, error: logger.info(
37
+ f"Anthropic API retry attempt {attempt} after {delay_ms}ms delay due to error: {str(error)}"
38
+ ),
39
+ )
40
+
41
+ def generate_completion(
42
+ self, options: LLMRequestOptions, history: List[LLMMessage]
43
+ ) -> LLMResponse:
44
+ try:
45
+ import httpx
46
+ except ImportError:
47
+ raise ImportError(
48
+ "Missing httpx. Install pytrilogy[ai] to use AnthropicProvider."
49
+ )
50
+
51
+ # Separate system messages from user/assistant messages
52
+ system_messages = [msg.content for msg in history if msg.role == "system"]
53
+ conversation_messages = [
54
+ {"role": msg.role, "content": msg.content}
55
+ for msg in history
56
+ if msg.role != "system"
57
+ ]
58
+
59
+ try:
60
+
61
+ def make_request():
62
+ with httpx.Client(timeout=60) as client:
63
+ payload = {
64
+ "model": self.model,
65
+ "messages": conversation_messages,
66
+ "max_tokens": options.max_tokens or DEFAULT_MAX_TOKENS,
67
+ # "temperature": options.temperature or 0.7,
68
+ # "top_p": options.top_p if hasattr(options, "top_p") else 1.0,
69
+ }
70
+
71
+ # Add system parameter if there are system messages
72
+ if system_messages:
73
+ # Combine multiple system messages with newlines
74
+ payload["system"] = "\n\n".join(system_messages)
75
+
76
+ response = client.post(
77
+ url=self.base_completion_url,
78
+ headers={
79
+ "Content-Type": "application/json",
80
+ "x-api-key": self.api_key,
81
+ "anthropic-version": "2023-06-01",
82
+ },
83
+ json=payload,
84
+ )
85
+ response.raise_for_status()
86
+ return response.json()
87
+
88
+ data = fetch_with_retry(make_request, self.retry_options)
89
+
90
+ return LLMResponse(
91
+ text=data["content"][0]["text"],
92
+ usage=UsageDict(
93
+ prompt_tokens=data["usage"]["input_tokens"],
94
+ completion_tokens=data["usage"]["output_tokens"],
95
+ total_tokens=data["usage"]["input_tokens"]
96
+ + data["usage"]["output_tokens"],
97
+ ),
98
+ )
99
+
100
+ except httpx.HTTPStatusError as error:
101
+ error_detail = error.response.text
102
+ raise Exception(
103
+ f"Anthropic API error ({error.response.status_code}): {error_detail}"
104
+ )
105
+ except Exception as error:
106
+ raise Exception(f"Anthropic API error: {str(error)}")
@@ -0,0 +1,24 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Optional
3
+
4
+ from trilogy.ai.enums import Provider
5
+ from trilogy.ai.models import LLMMessage, LLMRequestOptions, LLMResponse
6
+
7
+ RETRYABLE_CODES = [429, 500, 502, 503, 504]
8
+
9
+
10
+ class LLMProvider(ABC):
11
+ def __init__(self, name: str, api_key: str, model: str, provider: Provider):
12
+ self.api_key = api_key
13
+ self.models: List[str] = []
14
+ self.name = name
15
+ self.model = model
16
+ self.type = provider
17
+ self.error: Optional[str] = None
18
+
19
+ # Abstract method to be implemented by specific providers
20
+ @abstractmethod
21
+ def generate_completion(
22
+ self, options: LLMRequestOptions, history: List[LLMMessage]
23
+ ) -> LLMResponse:
24
+ pass
@@ -0,0 +1,146 @@
1
+ from os import environ
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from trilogy.ai.enums import Provider
5
+ from trilogy.ai.models import LLMMessage, LLMResponse, UsageDict
6
+ from trilogy.constants import logger
7
+
8
+ from .base import RETRYABLE_CODES, LLMProvider, LLMRequestOptions
9
+ from .utils import RetryOptions, fetch_with_retry
10
+
11
+
12
+ class GoogleProvider(LLMProvider):
13
+ def __init__(
14
+ self,
15
+ name: str,
16
+ model: str,
17
+ api_key: str | None = None,
18
+ retry_options: Optional[RetryOptions] = None,
19
+ ):
20
+ api_key = api_key or environ.get("GOOGLE_API_KEY")
21
+ if not api_key:
22
+ raise ValueError(
23
+ "API key argument or environment variable GOOGLE_API_KEY is required"
24
+ )
25
+ super().__init__(name, api_key, model, Provider.GOOGLE)
26
+ self.base_model_url = "https://generativelanguage.googleapis.com/v1/models"
27
+ self.base_completion_url = "https://generativelanguage.googleapis.com/v1beta"
28
+ self.models: List[str] = []
29
+ self.type = Provider.GOOGLE
30
+ self.retry_options = retry_options or RetryOptions(
31
+ max_retries=3,
32
+ initial_delay_ms=30000,
33
+ retry_status_codes=RETRYABLE_CODES,
34
+ on_retry=lambda attempt, delay_ms, error: logger.info(
35
+ f"Google API retry attempt {attempt} after {delay_ms}ms delay due to error: {str(error)}"
36
+ ),
37
+ )
38
+
39
+ def _convert_to_gemini_history(
40
+ self, messages: List[LLMMessage]
41
+ ) -> List[Dict[str, Any]]:
42
+ """Convert standard message format to Gemini format."""
43
+ return [
44
+ {
45
+ "role": "model" if msg.role == "assistant" else "user",
46
+ "parts": [{"text": msg.content}],
47
+ }
48
+ for msg in messages
49
+ ]
50
+
51
+ def generate_completion(
52
+ self, options: LLMRequestOptions, history: List[LLMMessage]
53
+ ) -> LLMResponse:
54
+ try:
55
+ import httpx
56
+ except ImportError:
57
+ raise ImportError(
58
+ "Missing httpx. Install pytrilogy[ai] to use GoogleProvider."
59
+ )
60
+
61
+ # Convert messages to Gemini format
62
+ gemini_history = self._convert_to_gemini_history(history)
63
+
64
+ # Separate system message if present
65
+ system_instruction = None
66
+ contents = gemini_history
67
+
68
+ # Check if first message is a system message
69
+ if history and history[0].role == "system":
70
+ system_instruction = {"parts": [{"text": history[0].content}]}
71
+ contents = gemini_history[1:] # Remove system message from history
72
+
73
+ # Build the request URL
74
+ url = f"{self.base_completion_url}/models/{self.model}:generateContent"
75
+
76
+ # Build request body
77
+ request_body: Dict[str, Any] = {"contents": contents, "generationConfig": {}}
78
+
79
+ # Add system instruction if present
80
+ if system_instruction:
81
+ request_body["systemInstruction"] = system_instruction
82
+
83
+ # Add generation config options
84
+ if options.temperature is not None:
85
+ request_body["generationConfig"]["temperature"] = options.temperature
86
+
87
+ if options.max_tokens is not None:
88
+ request_body["generationConfig"]["maxOutputTokens"] = options.max_tokens
89
+
90
+ if options.top_p is not None:
91
+ request_body["generationConfig"]["topP"] = options.top_p
92
+
93
+ try:
94
+ # Make the API request with retry logic using a lambda
95
+
96
+ def fetch_function() -> Dict[str, Any]:
97
+ with httpx.Client(timeout=60) as client:
98
+ resp = client.post(
99
+ url,
100
+ headers={
101
+ "Content-Type": "application/json",
102
+ "x-goog-api-key": self.api_key,
103
+ },
104
+ json=request_body,
105
+ )
106
+ resp.raise_for_status()
107
+ return resp.json()
108
+
109
+ data = fetch_with_retry(
110
+ fetch_fn=fetch_function,
111
+ options=self.retry_options,
112
+ )
113
+
114
+ # Extract text from response
115
+ candidates = data.get("candidates", [])
116
+ if not candidates:
117
+ raise Exception("No candidates returned from Google API")
118
+
119
+ content = candidates[0].get("content", {})
120
+ parts = content.get("parts", [])
121
+
122
+ if not parts:
123
+ raise Exception("No parts in response content")
124
+
125
+ text = parts[0].get("text", "")
126
+
127
+ # Extract usage metadata
128
+ usage_metadata = data.get("usageMetadata", {})
129
+ prompt_tokens = usage_metadata.get("promptTokenCount", 0)
130
+ completion_tokens = usage_metadata.get("candidatesTokenCount", 0)
131
+
132
+ return LLMResponse(
133
+ text=text,
134
+ usage=UsageDict(
135
+ prompt_tokens=prompt_tokens,
136
+ completion_tokens=completion_tokens,
137
+ total_tokens=prompt_tokens + completion_tokens,
138
+ ),
139
+ )
140
+ except httpx.HTTPStatusError as error:
141
+ error_detail = error.response.text
142
+ raise Exception(
143
+ f"Google API error ({error.response.status_code}): {error_detail}"
144
+ )
145
+ except Exception as error:
146
+ raise Exception(f"Google API error: {str(error)}")
@@ -0,0 +1,89 @@
1
+ from os import environ
2
+ from typing import List, Optional
3
+
4
+ from trilogy.ai.enums import Provider
5
+ from trilogy.ai.models import LLMMessage, LLMResponse, UsageDict
6
+ from trilogy.constants import logger
7
+
8
+ from .base import RETRYABLE_CODES, LLMProvider, LLMRequestOptions
9
+ from .utils import RetryOptions, fetch_with_retry
10
+
11
+
12
+ class OpenAIProvider(LLMProvider):
13
+ def __init__(
14
+ self,
15
+ name: str,
16
+ model: str,
17
+ api_key: str | None = None,
18
+ retry_options: Optional[RetryOptions] = None,
19
+ ):
20
+ api_key = api_key or environ.get("OPENAI_API_KEY")
21
+ if not api_key:
22
+ raise ValueError(
23
+ "API key argument or environment variable OPENAI_API_KEY is required"
24
+ )
25
+ super().__init__(name, api_key, model, Provider.OPENAI)
26
+ self.base_completion_url = "https://api.openai.com/v1/chat/completions"
27
+ self.base_model_url = "https://api.openai.com/v1/models"
28
+ self.models: List[str] = []
29
+ self.type = Provider.OPENAI
30
+
31
+ self.retry_options = retry_options or RetryOptions(
32
+ max_retries=3,
33
+ initial_delay_ms=1000,
34
+ retry_status_codes=RETRYABLE_CODES,
35
+ on_retry=lambda attempt, delay_ms, error: logger.info(
36
+ f"Retry attempt {attempt} after {delay_ms}ms delay due to error: {str(error)}"
37
+ ),
38
+ )
39
+
40
+ def generate_completion(
41
+ self, options: LLMRequestOptions, history: List[LLMMessage]
42
+ ) -> LLMResponse:
43
+ try:
44
+ import httpx
45
+ except ImportError:
46
+ raise ImportError(
47
+ "Missing httpx. Install pytrilogy[ai] to use OpenAIProvider."
48
+ )
49
+
50
+ messages: List[dict] = []
51
+ messages = [{"role": msg.role, "content": msg.content} for msg in history]
52
+ try:
53
+
54
+ def make_request():
55
+ with httpx.Client(timeout=30) as client:
56
+ payload = {
57
+ "model": self.model,
58
+ "messages": messages,
59
+ }
60
+
61
+ response = client.post(
62
+ url=self.base_completion_url,
63
+ headers={
64
+ "Content-Type": "application/json",
65
+ "Authorization": f"Bearer {self.api_key}",
66
+ },
67
+ json=payload,
68
+ )
69
+ response.raise_for_status()
70
+ return response.json()
71
+
72
+ data = fetch_with_retry(make_request, self.retry_options)
73
+ return LLMResponse(
74
+ text=data["choices"][0]["message"]["content"],
75
+ usage=UsageDict(
76
+ prompt_tokens=data["usage"]["prompt_tokens"],
77
+ completion_tokens=data["usage"]["completion_tokens"],
78
+ total_tokens=data["usage"]["total_tokens"],
79
+ ),
80
+ )
81
+ except httpx.HTTPStatusError as error:
82
+ # Capture the response body text
83
+ error_detail = error.response.text
84
+ raise Exception(
85
+ f"OpenAI API error ({error.response.status_code}): {error_detail}"
86
+ )
87
+
88
+ except Exception as error:
89
+ raise Exception(f"OpenAI API error: {str(error)}")
@@ -0,0 +1,68 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+ from typing import Callable, List, TypeVar
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ @dataclass
9
+ class RetryOptions:
10
+ max_retries: int = 3
11
+ initial_delay_ms: int = 1000
12
+ retry_status_codes: List[int] = field(
13
+ default_factory=lambda: [429, 500, 502, 503, 504, 525]
14
+ )
15
+ on_retry: Callable[[int, int, Exception], None] | None = None
16
+
17
+
18
+ def fetch_with_retry(fetch_fn: Callable[[], T], options: RetryOptions) -> T:
19
+ from httpx import HTTPError
20
+
21
+ """
22
+ Retry a fetch operation with exponential backoff.
23
+
24
+ Args:
25
+ fetch_fn: Function that performs the fetch operation
26
+ options: Retry configuration options
27
+
28
+ Returns:
29
+ The result from the successful fetch operation
30
+
31
+ Raises:
32
+ The last exception encountered if all retries fail
33
+ """
34
+ from httpx import HTTPStatusError, TimeoutException
35
+
36
+ last_error = None
37
+ delay_ms = options.initial_delay_ms
38
+
39
+ for attempt in range(options.max_retries + 1):
40
+ try:
41
+ return fetch_fn()
42
+ except (HTTPError, TimeoutException, Exception) as error:
43
+ last_error = error
44
+ should_retry = False
45
+
46
+ if isinstance(error, HTTPStatusError):
47
+ if (
48
+ options.retry_status_codes
49
+ and error.response.status_code in options.retry_status_codes
50
+ ):
51
+ should_retry = True
52
+ elif isinstance(error, TimeoutException):
53
+ should_retry = True
54
+ if not should_retry or attempt >= options.max_retries:
55
+ raise
56
+
57
+ # Call the retry callback if provided
58
+ if options.on_retry:
59
+ options.on_retry(attempt + 1, delay_ms, error)
60
+
61
+ # Wait before retrying with exponential backoff
62
+ time.sleep(delay_ms / 1000.0)
63
+ delay_ms *= 2 # backoff
64
+
65
+ # This should never be reached, but just in case
66
+ if last_error:
67
+ raise last_error
68
+ raise Exception("Retry logic failed unexpectedly")
@@ -0,0 +1,3 @@
1
+ ### Authoring Public Interface
2
+
3
+ For anyone directly building on Trilogy in python, this folder contains the public interface for authoring objects.
@@ -0,0 +1,143 @@
1
+ from trilogy.constants import DEFAULT_NAMESPACE
2
+ from trilogy.core.enums import (
3
+ BooleanOperator,
4
+ ComparisonOperator,
5
+ FunctionClass,
6
+ FunctionType,
7
+ InfiniteFunctionArgs,
8
+ Ordering,
9
+ Purpose,
10
+ )
11
+ from trilogy.core.functions import FunctionFactory
12
+ from trilogy.core.models.author import (
13
+ AggregateWrapper,
14
+ CaseElse,
15
+ CaseWhen,
16
+ Comparison,
17
+ Concept,
18
+ ConceptRef,
19
+ Conditional,
20
+ FilterItem,
21
+ Function,
22
+ FunctionCallWrapper,
23
+ HavingClause,
24
+ MagicConstants,
25
+ Metadata,
26
+ MultiSelectLineage,
27
+ OrderBy,
28
+ OrderItem,
29
+ Parenthetical,
30
+ RowsetItem,
31
+ SubselectComparison,
32
+ WhereClause,
33
+ WindowItem,
34
+ WindowItemOrder,
35
+ WindowItemOver,
36
+ WindowOrder,
37
+ WindowType,
38
+ )
39
+ from trilogy.core.models.core import (
40
+ ArrayType,
41
+ DataType,
42
+ ListWrapper,
43
+ MapType,
44
+ NumericType,
45
+ StructType,
46
+ TraitDataType,
47
+ )
48
+ from trilogy.core.models.datasource import Address, Datasource, DatasourceMetadata
49
+ from trilogy.core.models.environment import Environment
50
+ from trilogy.core.statements.author import (
51
+ STATEMENT_TYPES,
52
+ ConceptDeclarationStatement,
53
+ ConceptTransform,
54
+ CopyStatement,
55
+ Grain,
56
+ HasUUID,
57
+ ImportStatement,
58
+ MultiSelectStatement,
59
+ PersistStatement,
60
+ RawSQLStatement,
61
+ RowsetDerivationStatement,
62
+ SelectItem,
63
+ SelectStatement,
64
+ ShowCategory,
65
+ ShowStatement,
66
+ ValidateStatement,
67
+ )
68
+ from trilogy.parsing.common import arbitrary_to_concept, arg_to_datatype
69
+
70
+ __all__ = [
71
+ # trilogy.constants
72
+ "DEFAULT_NAMESPACE",
73
+ # trilogy.core.enums
74
+ "BooleanOperator",
75
+ "ComparisonOperator",
76
+ "FunctionClass",
77
+ "FunctionType",
78
+ "InfiniteFunctionArgs",
79
+ "Ordering",
80
+ "Purpose",
81
+ # trilogy.core.functions
82
+ "FunctionFactory",
83
+ # trilogy.core.models.author
84
+ "AggregateWrapper",
85
+ "CaseElse",
86
+ "CaseWhen",
87
+ "Comparison",
88
+ "Concept",
89
+ "ConceptRef",
90
+ "Conditional",
91
+ "FilterItem",
92
+ "Function",
93
+ "FunctionCallWrapper",
94
+ "HavingClause",
95
+ "MagicConstants",
96
+ "Metadata",
97
+ "MultiSelectLineage",
98
+ "OrderBy",
99
+ "OrderItem",
100
+ "Parenthetical",
101
+ "RowsetItem",
102
+ "SubselectComparison",
103
+ "WhereClause",
104
+ "WindowItem",
105
+ "WindowItemOrder",
106
+ "WindowItemOver",
107
+ "WindowOrder",
108
+ "WindowType",
109
+ # trilogy.core.models.core
110
+ "ArrayType",
111
+ "DataType",
112
+ "ListWrapper",
113
+ "MapType",
114
+ "NumericType",
115
+ "StructType",
116
+ "TraitDataType",
117
+ # trilogy.core.models.datasource
118
+ "Address",
119
+ "Datasource",
120
+ "DatasourceMetadata",
121
+ # trilogy.core.models.environment
122
+ "Environment",
123
+ # trilogy.core.statements.author
124
+ "ConceptDeclarationStatement",
125
+ "ConceptTransform",
126
+ "CopyStatement",
127
+ "Grain",
128
+ "HasUUID",
129
+ "ImportStatement",
130
+ "MultiSelectStatement",
131
+ "PersistStatement",
132
+ "RawSQLStatement",
133
+ "RowsetDerivationStatement",
134
+ "SelectItem",
135
+ "SelectStatement",
136
+ "ShowCategory",
137
+ "ShowStatement",
138
+ "ValidateStatement",
139
+ # trilogy.parsing.common
140
+ "arbitrary_to_concept",
141
+ "arg_to_datatype",
142
+ "STATEMENT_TYPES",
143
+ ]
trilogy/constants.py ADDED
@@ -0,0 +1,113 @@
1
+ import random
2
+ from contextlib import contextmanager
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from logging import getLogger
6
+ from typing import Any
7
+
8
+ logger = getLogger("trilogy")
9
+
10
+ DEFAULT_NAMESPACE = "local"
11
+
12
+ RECURSIVE_GATING_CONCEPT = "_terminal"
13
+
14
+ VIRTUAL_CONCEPT_PREFIX = "_virt"
15
+
16
+ ENV_CACHE_NAME = ".preql_cache.json"
17
+
18
+
19
+ class MagicConstants(Enum):
20
+ NULL = "null"
21
+ LINE_SEPARATOR = "\n"
22
+
23
+
24
+ NULL_VALUE = MagicConstants.NULL
25
+
26
+
27
+ @dataclass
28
+ class Optimizations:
29
+ predicate_pushdown: bool = True
30
+ datasource_inlining: bool = True
31
+ constant_inlining: bool = True
32
+ constant_inline_cutoff: int = 10
33
+ direct_return: bool = True
34
+ hide_unused_concepts: bool = True
35
+
36
+
37
+ @dataclass
38
+ class Comments:
39
+ """Control what is placed in CTE comments"""
40
+
41
+ show: bool = False
42
+ basic: bool = True
43
+ joins: bool = False
44
+ nullable: bool = False
45
+ partial: bool = False
46
+ source_map: bool = False
47
+
48
+
49
+ @dataclass
50
+ class Rendering:
51
+ """Control how the SQL is rendered"""
52
+
53
+ parameters: bool = True
54
+ concise: bool = False
55
+
56
+ @contextmanager
57
+ def temporary(self, **kwargs: Any):
58
+ """
59
+ Context manager to temporarily set attributes and revert them afterwards.
60
+
61
+ Usage:
62
+ r = Rendering()
63
+ with r.temporary(parameters=False, concise=True):
64
+ # parameters is False, concise is True here
65
+ do_something()
66
+ # parameters and concise are back to their original values
67
+ """
68
+ # Store original values
69
+ original_values = {key: getattr(self, key) for key in kwargs}
70
+
71
+ # Set new values
72
+ for key, value in kwargs.items():
73
+ setattr(self, key, value)
74
+
75
+ try:
76
+ yield self
77
+ finally:
78
+ # Restore original values
79
+ for key, value in original_values.items():
80
+ setattr(self, key, value)
81
+
82
+
83
+ @dataclass
84
+ class Parsing:
85
+ """Control Parsing"""
86
+
87
+ strict_name_shadow_enforcement: bool = False
88
+ select_as_definition: bool = True
89
+
90
+
91
+ # TODO: support loading from environments
92
+ @dataclass
93
+ class Config:
94
+ strict_mode: bool = True
95
+ human_identifiers: bool = True
96
+ randomize_cte_names: bool = False
97
+ validate_missing: bool = True
98
+ comments: Comments = field(default_factory=Comments)
99
+ optimizations: Optimizations = field(default_factory=Optimizations)
100
+ rendering: Rendering = field(default_factory=Rendering)
101
+ parsing: Parsing = field(default_factory=Parsing)
102
+
103
+ @property
104
+ def show_comments(self) -> bool:
105
+ return self.comments.show
106
+
107
+ def set_random_seed(self, seed: int):
108
+ random.seed(seed)
109
+
110
+
111
+ CONFIG = Config()
112
+
113
+ CONFIG.set_random_seed(42)