pytrilogy 0.0.3.77__py3-none-any.whl → 0.0.3.78__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.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.77
3
+ Version: 0.0.3.78
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -44,17 +44,20 @@ The Trilogy language is an experiment in better SQL for analytics - a streamline
44
44
 
45
45
  [pytrilogy](https://github.com/trilogy-data/pytrilogy) is the reference implementation, written in Python.
46
46
 
47
- Trilogy concretely solves these common problems in karge, SQL based analytics teams:
48
- - decoupling consumption code from specific physical assets
49
- - better testability and change management
50
- - reduced boilerplate and opportunity for OLAP style optimization at scale
47
+ ### What Trilogy Gives You
51
48
 
52
- Trilogy can be especially powerful as a frontend consumption language, since the decoupling from the physical layout makes dynamic and interactive dashboards backed by SQL tables much easier to create.
49
+ - Speed - write faster, with concise, powerful syntax
50
+ - Efficiency - write less SQL, and reuse what you do
51
+ - Fearless refactoring
52
+ - Testability
53
+ - Easy to use for humans and LLMs
54
+
55
+ Trilogy is epsecially targeted at data consumption, providing a rich metadata layer that makes visualizing Trilogy easy and expressive.
53
56
 
54
57
  > [!TIP]
55
58
  > You can try Trilogy in a [open-source studio](https://trilogydata.dev/trilogy-studio-core/). More details on the language can be found on the [documentation](https://trilogydata.dev/).
56
59
 
57
- We recommend the studio as the fastest way to explore Trilogy. For deeper work and integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
60
+ Start in the studio to explore Trilogy. For deeper work and integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
58
61
 
59
62
  Installation: `pip install pytrilogy`
60
63
 
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.77.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=MaNvYTE25EzVDkpklTQu2tZ8pSfm2j1aCH9wBRXrMpY,303
1
+ pytrilogy-0.0.3.78.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=eSf6G95oP2fg8vK_MLgUDxm7x3z05oIQBWOhr5hO8fg,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=eKb_EJvSqjN9tGbdVEViwdtwwh8fZ3-jpOEDqL71y70,1691
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
@@ -97,7 +97,7 @@ trilogy/parsing/common.py,sha256=yV1AckK0h8u1OFeGQBTMu-wuW5m63c5CcZuPicsTH_w,306
97
97
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
98
98
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
99
99
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
100
- trilogy/parsing/parse_engine.py,sha256=c8NzO4xR3egV-tKS02X6Ja5P4-QfWtnTzkfEn0eKYeU,76660
100
+ trilogy/parsing/parse_engine.py,sha256=zVPHfYYHH8n_zIDR0Esrhj17recy5142akiEItDOtEw,77987
101
101
  trilogy/parsing/render.py,sha256=HSNntD82GiiwHT-TWPLXAaIMWLYVV5B5zQEsgwrHIBE,19605
102
102
  trilogy/parsing/trilogy.lark,sha256=wiGXJdKfPTG7E_XdkN1rf9g9Yy1-UMVAXyTxtrBPm9w,15037
103
103
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -110,8 +110,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
110
110
  trilogy/std/net.preql,sha256=7l7MqIjs6TDCpO6dBAoNJU81Ex255jZRK36kBgE1GDs,158
111
111
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
112
112
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
113
- pytrilogy-0.0.3.77.dist-info/METADATA,sha256=LApdnLN-kkLR7hIwuXiUQuAS_cFqfzITIcXqME1p0Jo,9734
114
- pytrilogy-0.0.3.77.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
- pytrilogy-0.0.3.77.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
- pytrilogy-0.0.3.77.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
- pytrilogy-0.0.3.77.dist-info/RECORD,,
113
+ pytrilogy-0.0.3.78.dist-info/METADATA,sha256=nWclkhdYd5C_jRGPwLej4lpsxDB7vqDJNeU8P8tU7Es,9589
114
+ pytrilogy-0.0.3.78.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
+ pytrilogy-0.0.3.78.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
+ pytrilogy-0.0.3.78.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
+ pytrilogy-0.0.3.78.dist-info/RECORD,,
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.77"
7
+ __version__ = "0.0.3.78"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -2093,12 +2093,50 @@ def parse_text_raw(text: str, environment: Optional[Environment] = None):
2093
2093
 
2094
2094
  ERROR_CODES: dict[int, str] = {
2095
2095
  # 100 code are SQL compatability errors
2096
- 101: "Trilogy does not have a FROM clause (Datasource resolution is automatic). Remove the FROM clause.",
2096
+ 101: "Using FROM keyword? Trilogy does not have a FROM clause (Datasource resolution is automatic).",
2097
2097
  # 200 codes relate to required explicit syntax (we could loosen these?)
2098
- 201: 'Alias must be specified with "AS" - e.g. `SELECT x AS y`',
2099
- 210: "Order by must be explicit about direction - specify `asc` or `desc`.",
2098
+ 201: 'Missing alias? Alias must be specified with "AS" - e.g. `SELECT x+1 AS y`',
2099
+ 210: "Missing order direction? Order by must be explicit about direction - specify `asc` or `desc`.",
2100
2100
  }
2101
2101
 
2102
+ DEFAULT_ERROR_SPAN: int = 30
2103
+
2104
+
2105
+ def inject_context_maker(pos: int, text: str, span: int = 40) -> str:
2106
+ """Returns a pretty string pinpointing the error in the text,
2107
+ with span amount of context characters around it.
2108
+
2109
+ Note:
2110
+ The parser doesn't hold a copy of the text it has to parse,
2111
+ so you have to provide it again
2112
+ """
2113
+
2114
+ start = max(pos - span, 0)
2115
+ end = pos + span
2116
+ if not isinstance(text, bytes):
2117
+
2118
+ before = text[start:pos].rsplit("\n", 1)[-1]
2119
+ after = text[pos:end].split("\n", 1)[0]
2120
+ if end > len(text):
2121
+ rcap = ""
2122
+ elif not after[-1].isspace():
2123
+ rcap = "..."
2124
+ if start > 0 and not before[0].isspace():
2125
+ lcap = "..."
2126
+ else:
2127
+ lcap = ""
2128
+ lpad = " "
2129
+ rpad = " "
2130
+ if before.endswith(" "):
2131
+ lpad = ""
2132
+ if after.startswith(" "):
2133
+ rpad = ""
2134
+ return f"{lcap}{before}{lpad}???{rpad}{after}{rcap}"
2135
+ else:
2136
+ before = text[start:pos].rsplit(b"\n", 1)[-1]
2137
+ after = text[pos:end].split(b"\n", 1)[0]
2138
+ return (before + b" ??? " + after).decode("ascii", "backslashreplace")
2139
+
2102
2140
 
2103
2141
  def parse_text(
2104
2142
  text: str,
@@ -2117,6 +2155,48 @@ def parse_text(
2117
2155
  | None
2118
2156
  ],
2119
2157
  ]:
2158
+ def _create_syntax_error(code: int, pos: int, text: str) -> InvalidSyntaxException:
2159
+ """Helper to create standardized syntax error with context."""
2160
+ return InvalidSyntaxException(
2161
+ f"Syntax [{code}]: "
2162
+ + ERROR_CODES[code]
2163
+ + "\nLocation:\n"
2164
+ + inject_context_maker(pos, text.replace("\n", " "), DEFAULT_ERROR_SPAN)
2165
+ )
2166
+
2167
+ def _create_generic_syntax_error(
2168
+ message: str, pos: int, text: str
2169
+ ) -> InvalidSyntaxException:
2170
+ """Helper to create generic syntax error with context."""
2171
+ return InvalidSyntaxException(
2172
+ message
2173
+ + "\nLocation:\n"
2174
+ + inject_context_maker(pos, text.replace("\n", " "), DEFAULT_ERROR_SPAN)
2175
+ )
2176
+
2177
+ def _handle_unexpected_token(e: UnexpectedToken, text: str) -> None:
2178
+ """Handle UnexpectedToken errors with specific logic."""
2179
+ # Handle ordering direction error
2180
+ pos = e.pos_in_stream or 0
2181
+ if e.expected == {"ORDERING_DIRECTION"}:
2182
+ raise _create_syntax_error(210, pos, text)
2183
+
2184
+ # Handle FROM token error
2185
+ parsed_tokens = [x.value for x in e.token_history] if e.token_history else []
2186
+ if parsed_tokens == ["FROM"]:
2187
+ raise _create_syntax_error(101, pos, text)
2188
+
2189
+ # Attempt recovery for aliasing
2190
+ try:
2191
+ e.interactive_parser.feed_token(Token("AS", "AS"))
2192
+ e.interactive_parser.feed_token(Token("IDENTIFIER", e.token.value))
2193
+ raise _create_syntax_error(201, pos, text)
2194
+ except UnexpectedToken:
2195
+ pass
2196
+
2197
+ # Default UnexpectedToken handling
2198
+ raise _create_generic_syntax_error(str(e), pos, text)
2199
+
2120
2200
  environment = environment or (
2121
2201
  Environment(working_path=root) if root else Environment()
2122
2202
  )
@@ -2124,6 +2204,7 @@ def parse_text(
2124
2204
  environment=environment, import_keys=["root"], parse_config=parse_config
2125
2205
  )
2126
2206
  start = datetime.now()
2207
+
2127
2208
  try:
2128
2209
  parser.set_text(text)
2129
2210
  # disable fail on missing to allow for circular dependencies
@@ -2141,54 +2222,11 @@ def parse_text(
2141
2222
  unpack_visit_error(e, text)
2142
2223
  # this will never be reached
2143
2224
  raise e
2144
- except (
2145
- UnexpectedCharacters,
2146
- UnexpectedEOF,
2147
- UnexpectedInput,
2148
- UnexpectedToken,
2149
- ValidationError,
2150
- TypeError,
2151
- ) as e:
2152
- if isinstance(e, UnexpectedToken):
2153
- if e.expected == {"ORDERING_DIRECTION"}:
2154
- code = 210
2155
- raise InvalidSyntaxException(
2156
- f"Syntax [{code}]:"
2157
- + ERROR_CODES[code]
2158
- + "\nContext:\n"
2159
- + e.get_context(text.replace("\n", " "), 20)
2160
- )
2161
- parsed_tokens = (
2162
- [x.value for x in e.token_history] if e.token_history else []
2163
- )
2164
- if parsed_tokens == ["FROM"]:
2165
- code = 101
2166
- raise InvalidSyntaxException(
2167
- f"Syntax [{code}]:"
2168
- + ERROR_CODES[code]
2169
- + "\nContext:\n"
2170
- + e.get_context(text.replace("\n", " "), 20)
2171
- )
2172
- # recovery attempt for aliasing
2173
- try:
2174
- e.interactive_parser.feed_token(Token("AS", "AS"))
2175
- e.interactive_parser.feed_token(e.token)
2176
- raise InvalidSyntaxException(
2177
- f"Syntax [{ERROR_CODES[201]}]:"
2178
- + ERROR_CODES[201]
2179
- + "\nContext:\n"
2180
- + e.get_context(text.replace("\n", " "), 20)
2181
- )
2182
- except UnexpectedToken:
2183
- pass
2184
- raise InvalidSyntaxException(
2185
- str(e) + "\nContext:\n" + e.get_context(text.replace("\n", " "), 20)
2186
- )
2187
- elif isinstance(e, (UnexpectedCharacters, UnexpectedEOF, UnexpectedInput)):
2188
-
2189
- raise InvalidSyntaxException(
2190
- str(e) + "\nContext:\n" + e.get_context(text.replace("\n", " "), 20)
2191
- )
2225
+ except UnexpectedToken as e:
2226
+ _handle_unexpected_token(e, text)
2227
+ except (UnexpectedCharacters, UnexpectedEOF, UnexpectedInput) as e:
2228
+ raise _create_generic_syntax_error(str(e), e.pos_in_stream or 0, text)
2229
+ except (ValidationError, TypeError) as e:
2192
2230
  raise InvalidSyntaxException(str(e))
2193
2231
 
2194
2232
  return environment, output