pytrilogy 0.0.3.76__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.76
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.76.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=sZwmfj6wT6T8WzjItYxWbPez-bFEh_9z7q4rztVOGdE,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,9 +97,9 @@ 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=4XL-YOEXVZ77w8QKsvWq5gePMo-S-P3vIhhcsywbzNM,74827
100
+ trilogy/parsing/parse_engine.py,sha256=zVPHfYYHH8n_zIDR0Esrhj17recy5142akiEItDOtEw,77987
101
101
  trilogy/parsing/render.py,sha256=HSNntD82GiiwHT-TWPLXAaIMWLYVV5B5zQEsgwrHIBE,19605
102
- trilogy/parsing/trilogy.lark,sha256=v2UqMBhU9hn9km4mvQkk3E2wsEZjJ4cz4wU72NFFvU4,14993
102
+ trilogy/parsing/trilogy.lark,sha256=wiGXJdKfPTG7E_XdkN1rf9g9Yy1-UMVAXyTxtrBPm9w,15037
103
103
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
104
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
105
105
  trilogy/std/__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.76.dist-info/METADATA,sha256=NZ4JMPMai3zouER09hbjd6F2cTwSbTqAIzQSR7JmyjM,9734
114
- pytrilogy-0.0.3.76.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
- pytrilogy-0.0.3.76.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
116
- pytrilogy-0.0.3.76.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
117
- pytrilogy-0.0.3.76.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.76"
7
+ __version__ = "0.0.3.78"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from re import IGNORECASE
8
8
  from typing import Any, List, Optional, Tuple, Union
9
9
 
10
- from lark import Lark, ParseTree, Transformer, Tree, v_args
10
+ from lark import Lark, ParseTree, Token, Transformer, Tree, v_args
11
11
  from lark.exceptions import (
12
12
  UnexpectedCharacters,
13
13
  UnexpectedEOF,
@@ -2091,6 +2091,53 @@ def parse_text_raw(text: str, environment: Optional[Environment] = None):
2091
2091
  PARSER.parse(text)
2092
2092
 
2093
2093
 
2094
+ ERROR_CODES: dict[int, str] = {
2095
+ # 100 code are SQL compatability errors
2096
+ 101: "Using FROM keyword? Trilogy does not have a FROM clause (Datasource resolution is automatic).",
2097
+ # 200 codes relate to required explicit syntax (we could loosen these?)
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
+ }
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
+
2140
+
2094
2141
  def parse_text(
2095
2142
  text: str,
2096
2143
  environment: Optional[Environment] = None,
@@ -2108,6 +2155,48 @@ def parse_text(
2108
2155
  | None
2109
2156
  ],
2110
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
+
2111
2200
  environment = environment or (
2112
2201
  Environment(working_path=root) if root else Environment()
2113
2202
  )
@@ -2115,6 +2204,7 @@ def parse_text(
2115
2204
  environment=environment, import_keys=["root"], parse_config=parse_config
2116
2205
  )
2117
2206
  start = datetime.now()
2207
+
2118
2208
  try:
2119
2209
  parser.set_text(text)
2120
2210
  # disable fail on missing to allow for circular dependencies
@@ -2132,20 +2222,11 @@ def parse_text(
2132
2222
  unpack_visit_error(e, text)
2133
2223
  # this will never be reached
2134
2224
  raise e
2135
- except (
2136
- UnexpectedCharacters,
2137
- UnexpectedEOF,
2138
- UnexpectedInput,
2139
- UnexpectedToken,
2140
- ValidationError,
2141
- TypeError,
2142
- ) as e:
2143
- if isinstance(
2144
- e, (UnexpectedCharacters, UnexpectedEOF, UnexpectedInput, UnexpectedToken)
2145
- ):
2146
- raise InvalidSyntaxException(
2147
- str(e) + "\nContext:\n" + e.get_context(text.replace("\n", " "), 20)
2148
- )
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:
2149
2230
  raise InvalidSyntaxException(str(e))
2150
2231
 
2151
2232
  return environment, output
@@ -130,7 +130,9 @@
130
130
 
131
131
  over_list: concept_lit ("," concept_lit )* ","?
132
132
 
133
- !ordering: /ASC|DESC/i ("NULLS"i /FIRST|LAST|AUTO/i )?
133
+ ORDERING_DIRECTION: /ASC|DESC/i
134
+
135
+ !ordering: ORDERING_DIRECTION ("NULLS"i /FIRST|LAST|AUTO/i )?
134
136
 
135
137
  order_by: "ORDER"i "BY"i order_list
136
138