Skip to content

Commit 75517fc

Browse files
authored
Merge pull request #73 from passren/0.7.5
0.7.5
2 parents e6f21e7 + 2af6b11 commit 75517fc

15 files changed

Lines changed: 321 additions & 60 deletions

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111

1212
env:
13-
PYTHON_VERSION: "3.9"
13+
PYTHON_VERSION: "3.13"
1414

1515
steps:
1616
- name: Checkout

.github/workflows/run-test.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
fail-fast: false
2121
matrix:
22-
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
22+
python-version: [3.9, "3.10", "3.11", "3.12", "3.13", "3.14"]
2323

2424
steps:
2525
- name: Checkout
@@ -35,7 +35,7 @@ jobs:
3535
pip install boto3
3636
pip install tenacity
3737
pip install pyparsing
38-
pip install sqlean.py==3.45.1
38+
pip install sqlean.py
3939
- name: black
4040
run: |
4141
pip install black

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Requirements
7070
--------------
7171
* Python
7272

73-
- CPython 3.8 3.9 3.10 3.11 3.12
73+
- CPython 3.9 3.10 3.11 3.12 3.13 3.14
7474

7575
Dependencies
7676
--------------

pydynamodb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
if TYPE_CHECKING:
77
from .connection import Connection
88

9-
__version__: str = "0.7.4"
9+
__version__: str = "0.7.5"
1010

1111
# Globals https://www.python.org/dev/peps/pep-0249/#globals
1212
apilevel: str = "2.0"

pydynamodb/sql/common.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Combine,
99
Regex,
1010
CaselessKeyword,
11+
Literal,
1112
Opt,
1213
one_of,
1314
Group,
@@ -109,14 +110,8 @@ class KeyWords:
109110
LESS,
110111
GREATER_OR_EQUAL,
111112
LESS_OR_EQUAL,
112-
AND,
113-
BETWEEN,
114-
IN,
115-
IS,
116-
NOT,
117-
OR,
118113
) = map(
119-
CaselessKeyword,
114+
Literal,
120115
[
121116
"+",
122117
"-",
@@ -126,6 +121,18 @@ class KeyWords:
126121
"<",
127122
">=",
128123
"<=",
124+
],
125+
)
126+
(
127+
AND,
128+
BETWEEN,
129+
IN,
130+
IS,
131+
NOT,
132+
OR,
133+
) = map(
134+
CaselessKeyword,
135+
[
129136
"AND",
130137
"BETWEEN",
131138
"IN",
@@ -351,6 +358,13 @@ def get_query_type(sql: str) -> QueryType:
351358
raise LookupError("Not supported query type")
352359

353360

361+
def escape_keyword(word: str) -> str:
362+
if word.upper() in RESERVED_WORDS:
363+
return f'"{word}"'
364+
else:
365+
return word
366+
367+
354368
# DynamoDB reserved words
355369
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
356370
RESERVED_WORDS = [

pydynamodb/sql/dml_sql.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABCMeta
44
from .base import Base
55
from typing import Any, Dict, List, Optional
6-
from .common import KeyWords, Tokens
6+
from .common import KeyWords, Tokens, escape_keyword
77
from .util import flatten_list
88
from pyparsing import (
99
ParseResults,
@@ -28,6 +28,9 @@
2828

2929

3030
class DmlBase(Base):
31+
def __escape_column(self, token: Any) -> str:
32+
return escape_keyword(token.get("column_name"))
33+
3134
_CONSISTENT_READ, _RETURN_CONSUMED_CAPACITY = map(
3235
CaselessKeyword,
3336
[
@@ -36,7 +39,9 @@ class DmlBase(Base):
3639
],
3740
)
3841

39-
ATTR_NAME = Opt('"') + Word(alphanums + "_-") + Opt('"')
42+
ATTR_NAME = (
43+
Opt('"') + Word(alphanums + "_-")("attr_name").set_name("attr_name") + Opt('"')
44+
)
4045
ATTR_ARRAY_NAME = ATTR_NAME + "[" + Word(nums) + "]"
4146

4247
_COLUMN_NAME = (
@@ -46,7 +51,7 @@ class DmlBase(Base):
4651

4752
_ALIAS_NAME = Word(alphanums + "_-")("alias_name").set_name("alias_name")
4853

49-
_COLUMN = _COLUMN_NAME
54+
_COLUMN = _COLUMN_NAME.set_parse_action(__escape_column)
5055

5156
_COLUMNS = delimited_list(
5257
Group(

pydynamodb/sql/dml_update.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,46 @@
2121
WHERE Artist='Acme Band' AND SongTitle='PartiQL Rocks'
2222
"""
2323
import logging
24-
import re
2524
from .dml_sql import DmlBase
25+
from .json_parser import jsonArray, jsonObject
2626
from .common import KeyWords, Tokens
27-
from pyparsing import Forward, Group, OneOrMore, Opt, Regex
27+
from .util import flatten_list
28+
from pyparsing import (
29+
Forward,
30+
Group,
31+
OneOrMore,
32+
Opt,
33+
Literal,
34+
ZeroOrMore,
35+
)
2836
from typing import Any, Dict
2937

3038
_logger = logging.getLogger(__name__) # type: ignore
3139

3240

3341
class DmlUpdate(DmlBase):
3442

35-
# Define SET operation: SET followed by content until next SET/REMOVE/WHERE
43+
_COMMA = Literal(",")
44+
45+
_COLUMN_UPDATE_RVAL = (jsonObject | jsonArray | DmlBase._COLUMN_RVAL)(
46+
"column_update_rvalue"
47+
).set_name("column_update_rvalue")
48+
49+
_OPERATION_CONTENT = Group(
50+
DmlBase._COLUMN + KeyWords.EQUAL_TO + _COLUMN_UPDATE_RVAL
51+
)("op_content").set_name("op_content")
52+
3653
_SET_OPERATION = Group(
37-
KeyWords.SET
38-
+ Regex(r".*?(?=\s+(?:SET|REMOVE|WHERE))", re.IGNORECASE | re.DOTALL)(
39-
"set_content"
40-
).set_name("set_content")
41-
)("set_op")
54+
KeyWords.SET + _OPERATION_CONTENT + ZeroOrMore(_COMMA + _OPERATION_CONTENT)
55+
)("set_op").set_name("set_op")
4256

43-
# Define REMOVE operation: REMOVE followed by content until next SET/REMOVE/WHERE
4457
_REMOVE_OPERATION = Group(
45-
KeyWords.REMOVE
46-
+ Regex(r".*?(?=\s+(?:SET|REMOVE|WHERE))", re.IGNORECASE | re.DOTALL)(
47-
"remove_content"
48-
).set_name("remove_content")
49-
)("remove_op")
58+
KeyWords.REMOVE + _OPERATION_CONTENT + ZeroOrMore(_COMMA + _OPERATION_CONTENT)
59+
)("remove_op").set_name("remove_op")
5060

51-
# Multiple SET or REMOVE operations
52-
_OPERATIONS = Group(OneOrMore(_SET_OPERATION | _REMOVE_OPERATION))("operations")
61+
_OPERATIONS = OneOrMore(_SET_OPERATION | _REMOVE_OPERATION)("operations").set_name(
62+
"operations"
63+
)
5364

5465
_UPDATE_STATEMENT = (
5566
KeyWords.UPDATE
@@ -76,15 +87,7 @@ def transform(self) -> Dict[str, Any]:
7687

7788
table_ = '"%s"' % table_name_
7889

79-
# Build the operations part from multiple SET/REMOVE operations
80-
operations_parts = []
81-
for op in operations_:
82-
if "set_op" == op.get_name():
83-
operations_parts.append("SET %s" % op["set_content"].strip())
84-
elif "remove_op" == op.get_name():
85-
operations_parts.append("REMOVE %s" % op["remove_content"].strip())
86-
87-
operations_str = " ".join(operations_parts)
90+
operations_str = " ".join(str(c) for c in flatten_list(operations_.as_list()))
8891

8992
where_conditions = self.root_parse_results.get("where_conditions", None)
9093
if where_conditions is not None:

pydynamodb/sql/json_parser.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# jsonParser.py
2+
#
3+
# Implementation of a simple JSON parser, returning a hierarchical
4+
# ParseResults object support both list- and dict-style data access.
5+
#
6+
# Copyright 2006, by Paul McGuire
7+
#
8+
# Updated 8 Jan 2007 - fixed dict grouping bug, and made elements and
9+
# members optional in array and object collections
10+
#
11+
# Updated 9 Aug 2016 - use more current pyparsing constructs/idioms
12+
#
13+
'''
14+
json_bnf = """
15+
object
16+
{ members }
17+
{}
18+
members
19+
string : value
20+
members , string : value
21+
array
22+
[ elements ]
23+
[]
24+
elements
25+
value
26+
elements , value
27+
value
28+
string
29+
number
30+
object
31+
array
32+
true
33+
false
34+
null
35+
"""
36+
'''
37+
38+
import pyparsing as pp
39+
from pyparsing import pyparsing_common as ppc
40+
41+
42+
def make_keyword(kwd_str, kwd_value):
43+
return pp.Keyword(kwd_str).set_parse_action(pp.replace_with(kwd_value))
44+
45+
46+
# set to False to return ParseResults
47+
RETURN_PYTHON_COLLECTIONS = True
48+
49+
TRUE = make_keyword("true", True)
50+
FALSE = make_keyword("false", False)
51+
NULL = make_keyword("null", None)
52+
53+
LBRACK, RBRACK, LBRACE, RBRACE, COLON = map(pp.Suppress, "[]{}:")
54+
55+
jsonString = (pp.dbl_quoted_string() | pp.sgl_quoted_string).set_parse_action(
56+
pp.remove_quotes
57+
)
58+
jsonNumber = ppc.number().set_name("jsonNumber")
59+
60+
jsonObject = pp.Forward().set_name("jsonObject")
61+
jsonValue = pp.Forward().set_name("jsonValue")
62+
63+
jsonElements = pp.DelimitedList(jsonValue).set_name(None)
64+
# jsonArray = pp.Group(LBRACK + pp.Optional(jsonElements, []) + RBRACK)
65+
# jsonValue << (
66+
# jsonString | jsonNumber | pp.Group(jsonObject) | jsonArray | TRUE | FALSE | NULL
67+
# )
68+
# memberDef = pp.Group(jsonString + COLON + jsonValue).set_name("jsonMember")
69+
70+
jsonArray = pp.Group(
71+
LBRACK + pp.Optional(jsonElements) + RBRACK, aslist=RETURN_PYTHON_COLLECTIONS
72+
).set_name("jsonArray")
73+
74+
jsonValue << (jsonString | jsonNumber | jsonObject | jsonArray | TRUE | FALSE | NULL)
75+
76+
memberDef = pp.Group(
77+
jsonString + COLON + jsonValue, aslist=RETURN_PYTHON_COLLECTIONS
78+
).set_name("jsonMember")
79+
80+
jsonMembers = pp.DelimitedList(memberDef).set_name(None)
81+
# jsonObject << pp.Dict(LBRACE + pp.Optional(jsonMembers) + RBRACE)
82+
jsonObject << pp.Dict(
83+
LBRACE + pp.Optional(jsonMembers) + RBRACE, asdict=RETURN_PYTHON_COLLECTIONS
84+
)
85+
86+
jsonComment = pp.cpp_style_comment
87+
jsonObject.ignore(jsonComment)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ boto3>=1.21.0
22
botocore>=1.24.7
33
tenacity>=4.1.0
44
pyparsing>=3.0.0
5-
sqlean.py==3.45.1
5+
sqlean.py>=3.45.0

setup.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"botocore>=1.24.7",
1515
"tenacity>=4.1.0",
1616
"pyparsing>=3.0.0",
17-
"sqlean.py==3.45.1",
17+
"sqlean.py>=3.45.0",
1818
]
1919

2020
extras_require = {
@@ -31,17 +31,18 @@
3131
author="Peng Ren",
3232
author_email="passren9099@hotmail.com",
3333
license="MIT",
34-
python_requires=">=3.8",
34+
python_requires=">=3.9",
3535
classifiers=[
36-
"Development Status :: 4 - Beta",
36+
"Development Status :: 5 - Production/Stable",
3737
"Intended Audience :: Developers",
3838
"Programming Language :: Python",
3939
"Programming Language :: Python",
40-
"Programming Language :: Python :: 3.8",
4140
"Programming Language :: Python :: 3.9",
4241
"Programming Language :: Python :: 3.10",
4342
"Programming Language :: Python :: 3.11",
4443
"Programming Language :: Python :: 3.12",
44+
"Programming Language :: Python :: 3.13",
45+
"Programming Language :: Python :: 3.14",
4546
"Operating System :: OS Independent",
4647
"License :: OSI Approved :: MIT License",
4748
],

0 commit comments

Comments
 (0)