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
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest

env:
PYTHON_VERSION: "3.9"
PYTHON_VERSION: "3.13"

steps:
- name: Checkout
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/run-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
python-version: [3.9, "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- name: Checkout
Expand All @@ -35,7 +35,7 @@ jobs:
pip install boto3
pip install tenacity
pip install pyparsing
pip install sqlean.py==3.45.1
pip install sqlean.py
- name: black
run: |
pip install black
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Requirements
--------------
* Python

- CPython 3.8 3.9 3.10 3.11 3.12
- CPython 3.9 3.10 3.11 3.12 3.13 3.14

Dependencies
--------------
Expand Down
2 changes: 1 addition & 1 deletion pydynamodb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
if TYPE_CHECKING:
from .connection import Connection

__version__: str = "0.7.4"
__version__: str = "0.7.5"

# Globals https://www.python.org/dev/peps/pep-0249/#globals
apilevel: str = "2.0"
Expand Down
28 changes: 21 additions & 7 deletions pydynamodb/sql/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
Combine,
Regex,
CaselessKeyword,
Literal,
Opt,
one_of,
Group,
Expand Down Expand Up @@ -109,14 +110,8 @@ class KeyWords:
LESS,
GREATER_OR_EQUAL,
LESS_OR_EQUAL,
AND,
BETWEEN,
IN,
IS,
NOT,
OR,
) = map(
CaselessKeyword,
Literal,
[
"+",
"-",
Expand All @@ -126,6 +121,18 @@ class KeyWords:
"<",
">=",
"<=",
],
)
(
AND,
BETWEEN,
IN,
IS,
NOT,
OR,
) = map(
CaselessKeyword,
[
"AND",
"BETWEEN",
"IN",
Expand Down Expand Up @@ -351,6 +358,13 @@ def get_query_type(sql: str) -> QueryType:
raise LookupError("Not supported query type")


def escape_keyword(word: str) -> str:
if word.upper() in RESERVED_WORDS:
return f'"{word}"'
else:
return word


# DynamoDB reserved words
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
RESERVED_WORDS = [
Expand Down
11 changes: 8 additions & 3 deletions pydynamodb/sql/dml_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from abc import ABCMeta
from .base import Base
from typing import Any, Dict, List, Optional
from .common import KeyWords, Tokens
from .common import KeyWords, Tokens, escape_keyword
from .util import flatten_list
from pyparsing import (
ParseResults,
Expand All @@ -28,6 +28,9 @@


class DmlBase(Base):
def __escape_column(self, token: Any) -> str:
return escape_keyword(token.get("column_name"))

_CONSISTENT_READ, _RETURN_CONSUMED_CAPACITY = map(
CaselessKeyword,
[
Expand All @@ -36,7 +39,9 @@ class DmlBase(Base):
],
)

ATTR_NAME = Opt('"') + Word(alphanums + "_-") + Opt('"')
ATTR_NAME = (
Opt('"') + Word(alphanums + "_-")("attr_name").set_name("attr_name") + Opt('"')
)
ATTR_ARRAY_NAME = ATTR_NAME + "[" + Word(nums) + "]"

_COLUMN_NAME = (
Expand All @@ -46,7 +51,7 @@ class DmlBase(Base):

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

_COLUMN = _COLUMN_NAME
_COLUMN = _COLUMN_NAME.set_parse_action(__escape_column)

_COLUMNS = delimited_list(
Group(
Expand Down
53 changes: 28 additions & 25 deletions pydynamodb/sql/dml_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,46 @@
WHERE Artist='Acme Band' AND SongTitle='PartiQL Rocks'
"""
import logging
import re
from .dml_sql import DmlBase
from .json_parser import jsonArray, jsonObject
from .common import KeyWords, Tokens
from pyparsing import Forward, Group, OneOrMore, Opt, Regex
from .util import flatten_list
from pyparsing import (
Forward,
Group,
OneOrMore,
Opt,
Literal,
ZeroOrMore,
)
from typing import Any, Dict

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


class DmlUpdate(DmlBase):

# Define SET operation: SET followed by content until next SET/REMOVE/WHERE
_COMMA = Literal(",")

_COLUMN_UPDATE_RVAL = (jsonObject | jsonArray | DmlBase._COLUMN_RVAL)(
"column_update_rvalue"
).set_name("column_update_rvalue")

_OPERATION_CONTENT = Group(
DmlBase._COLUMN + KeyWords.EQUAL_TO + _COLUMN_UPDATE_RVAL
)("op_content").set_name("op_content")

_SET_OPERATION = Group(
KeyWords.SET
+ Regex(r".*?(?=\s+(?:SET|REMOVE|WHERE))", re.IGNORECASE | re.DOTALL)(
"set_content"
).set_name("set_content")
)("set_op")
KeyWords.SET + _OPERATION_CONTENT + ZeroOrMore(_COMMA + _OPERATION_CONTENT)
)("set_op").set_name("set_op")

# Define REMOVE operation: REMOVE followed by content until next SET/REMOVE/WHERE
_REMOVE_OPERATION = Group(
KeyWords.REMOVE
+ Regex(r".*?(?=\s+(?:SET|REMOVE|WHERE))", re.IGNORECASE | re.DOTALL)(
"remove_content"
).set_name("remove_content")
)("remove_op")
KeyWords.REMOVE + _OPERATION_CONTENT + ZeroOrMore(_COMMA + _OPERATION_CONTENT)
)("remove_op").set_name("remove_op")

# Multiple SET or REMOVE operations
_OPERATIONS = Group(OneOrMore(_SET_OPERATION | _REMOVE_OPERATION))("operations")
_OPERATIONS = OneOrMore(_SET_OPERATION | _REMOVE_OPERATION)("operations").set_name(
"operations"
)

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

table_ = '"%s"' % table_name_

# Build the operations part from multiple SET/REMOVE operations
operations_parts = []
for op in operations_:
if "set_op" == op.get_name():
operations_parts.append("SET %s" % op["set_content"].strip())
elif "remove_op" == op.get_name():
operations_parts.append("REMOVE %s" % op["remove_content"].strip())

operations_str = " ".join(operations_parts)
operations_str = " ".join(str(c) for c in flatten_list(operations_.as_list()))

where_conditions = self.root_parse_results.get("where_conditions", None)
if where_conditions is not None:
Expand Down
87 changes: 87 additions & 0 deletions pydynamodb/sql/json_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# jsonParser.py
#
# Implementation of a simple JSON parser, returning a hierarchical
# ParseResults object support both list- and dict-style data access.
#
# Copyright 2006, by Paul McGuire
#
# Updated 8 Jan 2007 - fixed dict grouping bug, and made elements and
# members optional in array and object collections
#
# Updated 9 Aug 2016 - use more current pyparsing constructs/idioms
#
'''
json_bnf = """
object
{ members }
{}
members
string : value
members , string : value
array
[ elements ]
[]
elements
value
elements , value
value
string
number
object
array
true
false
null
"""
'''

import pyparsing as pp
from pyparsing import pyparsing_common as ppc


def make_keyword(kwd_str, kwd_value):
return pp.Keyword(kwd_str).set_parse_action(pp.replace_with(kwd_value))


# set to False to return ParseResults
RETURN_PYTHON_COLLECTIONS = True

TRUE = make_keyword("true", True)
FALSE = make_keyword("false", False)
NULL = make_keyword("null", None)

LBRACK, RBRACK, LBRACE, RBRACE, COLON = map(pp.Suppress, "[]{}:")

jsonString = (pp.dbl_quoted_string() | pp.sgl_quoted_string).set_parse_action(
pp.remove_quotes
)
jsonNumber = ppc.number().set_name("jsonNumber")

jsonObject = pp.Forward().set_name("jsonObject")
jsonValue = pp.Forward().set_name("jsonValue")

jsonElements = pp.DelimitedList(jsonValue).set_name(None)
# jsonArray = pp.Group(LBRACK + pp.Optional(jsonElements, []) + RBRACK)
# jsonValue << (
# jsonString | jsonNumber | pp.Group(jsonObject) | jsonArray | TRUE | FALSE | NULL
# )
# memberDef = pp.Group(jsonString + COLON + jsonValue).set_name("jsonMember")

jsonArray = pp.Group(
LBRACK + pp.Optional(jsonElements) + RBRACK, aslist=RETURN_PYTHON_COLLECTIONS
).set_name("jsonArray")

jsonValue << (jsonString | jsonNumber | jsonObject | jsonArray | TRUE | FALSE | NULL)

memberDef = pp.Group(
jsonString + COLON + jsonValue, aslist=RETURN_PYTHON_COLLECTIONS
).set_name("jsonMember")

jsonMembers = pp.DelimitedList(memberDef).set_name(None)
# jsonObject << pp.Dict(LBRACE + pp.Optional(jsonMembers) + RBRACE)
jsonObject << pp.Dict(
LBRACE + pp.Optional(jsonMembers) + RBRACE, asdict=RETURN_PYTHON_COLLECTIONS
)

jsonComment = pp.cpp_style_comment
jsonObject.ignore(jsonComment)
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ boto3>=1.21.0
botocore>=1.24.7
tenacity>=4.1.0
pyparsing>=3.0.0
sqlean.py==3.45.1
sqlean.py>=3.45.0
9 changes: 5 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"botocore>=1.24.7",
"tenacity>=4.1.0",
"pyparsing>=3.0.0",
"sqlean.py==3.45.1",
"sqlean.py>=3.45.0",
]

extras_require = {
Expand All @@ -31,17 +31,18 @@
author="Peng Ren",
author_email="passren9099@hotmail.com",
license="MIT",
python_requires=">=3.8",
python_requires=">=3.9",
classifiers=[
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
],
Expand Down
Loading
Loading