diff --git a/README.rst b/README.rst index c3b4305..015d193 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ Basic ^^^^^ .. code-block:: python - from imap_tools import MailBox, Q + from imap_tools import MailBox, AND # get list of email subjects from INBOX folder with MailBox('imap.mail.com').login('test@mail.com', 'password') as mailbox: @@ -42,7 +42,7 @@ Basic # get list of email subjects from INBOX folder - equivalent verbose version mailbox = MailBox('imap.mail.com') mailbox.login('test@mail.com', 'password', initial_folder='INBOX') # or mailbox.folder.set instead 3d arg - subjects = [msg.subject for msg in mailbox.fetch(Q(all=True))] + subjects = [msg.subject for msg in mailbox.fetch(AND(all=True))] mailbox.logout() MailBox/MailBoxUnencrypted for create mailbox instance. @@ -101,38 +101,42 @@ Possible search approaches: .. code-block:: python - from imap_tools import Q, AND, OR, NOT + from imap_tools import AND, OR, NOT - mailbox.fetch(Q(subject='weather')) # query, the str-like object - see below + mailbox.fetch(AND(subject='weather')) # query, the str-like object - see below mailbox.fetch('TEXT "hello"') # str, use charset arg for non US-ASCII chars mailbox.fetch(b'TEXT "\xd1\x8f"') # bytes, charset arg is ignored Implemented query builder for search logic described in `rfc3501 `_. See `query examples `_. -* Class AND and its alias Q are used to combine keys by the logical "and" condition. -* Class OR is used to combine keys by the logical "or" condition. -* Class NOT is used to invert the result of a logical expression. -* Class H (Header) is used to search by headers. +====== ===== ========================================== ============================================================ +Class Alias Usage Arguments +====== ===== ========================================== ============================================================ +AND A combines keys by logical "AND" condition Search keys (see below) | str +OR O combines keys by logical "OR" condition Search keys (see below) | str +NOT N invert the result of a logical expression AND/OR instances | str +Header H for search by headers name: str, value: str +====== ===== ========================================== ============================================================ If the "charset" argument is specified in MailBox.fetch, the search string will be encoded to this encoding. You can change this behavior by overriding MailBox._criteria_encoder or pass criteria as bytes in desired encoding. .. code-block:: python - from imap_tools import Q, AND, OR, NOT + from imap_tools import A, AND, OR, NOT # AND - Q(text='hello', new=True) # '(TEXT "hello" NEW)' + A(text='hello', new=True) # '(TEXT "hello" NEW)' # OR OR(text='hello', date=datetime.date(2000, 3, 15)) # '(OR TEXT "hello" ON 15-Mar-2000)' # NOT NOT(text='hello', new=True) # 'NOT (TEXT "hello" NEW)' # complex - Q(OR(from_='from@ya.ru', text='"the text"'), NOT(OR(Q(answered=False), Q(new=True))), to='to@ya.ru') + A(OR(from_='from@ya.ru', text='"the text"'), NOT(OR(A(answered=False), A(new=True))), to='to@ya.ru') # encoding - mailbox.fetch(Q(subject='привет'), charset='utf8') # 'привет' will be encoded by MailBox._criteria_encoder - # python note: you can't do: Q(text='two', NOT(subject='one')) - Q(NOT(subject='one'), text='two') # use kwargs after logic classes + mailbox.fetch(A(subject='привет'), charset='utf8') # 'привет' will be encoded by MailBox._criteria_encoder + # python note: you can't do: A(text='two', NOT(subject='one')) + A(NOT(subject='one'), text='two') # use kwargs after logic classes (args) The search key types are marked with `*` can accepts a sequence of values like list, tuple, set or generator. diff --git a/examples/search.py b/examples/search.py index 997737f..c0d0b5c 100644 --- a/examples/search.py +++ b/examples/search.py @@ -20,7 +20,7 @@ """ import datetime as dt -from imap_tools import AND, OR, NOT, Q, H +from imap_tools import AND, OR, NOT, A, H # date in the date list (date=date1 OR date=date3 OR date=date2) q1 = OR(date=[dt.date(2019, 10, 1), dt.date(2019, 10, 10), dt.date(2019, 10, 15)]) @@ -31,7 +31,7 @@ # "NOT ((OR OR ON 1-Oct-2019 ON 10-Oct-2019 ON 15-Oct-2019))" # subject contains "hello" AND date greater than or equal dt.date(2019, 10, 10) -q3 = Q(subject='hello', date_gte=dt.date(2019, 10, 10)) +q3 = A(subject='hello', date_gte=dt.date(2019, 10, 10)) # "(SUBJECT "hello" SINCE 10-Oct-2019)" # from contains one of the address parts @@ -51,9 +51,9 @@ # "(OR (OR TEXT "tag15" SUBJECT "tag15") (OR TEXT "tag10" SUBJECT "tag10"))" # header IsSpam contains '++' AND header CheckAntivirus contains '-' -q8 = Q(header=[H('IsSpam', '++'), H('CheckAntivirus', '-')]) +q8 = A(header=[H('IsSpam', '++'), H('CheckAntivirus', '-')]) # "(HEADER "IsSpam" "++" HEADER "CheckAntivirus" "-")" # complex from README -q9 = Q(OR(from_='from@ya.ru', text='"the text"'), NOT(OR(Q(answered=False), Q(new=True))), to='to@ya.ru') +q9 = A(OR(from_='from@ya.ru', text='"the text"'), NOT(OR(A(answered=False), A(new=True))), to='to@ya.ru') # "((OR FROM "from@ya.ru" TEXT "\\"the text\\"") NOT ((OR (UNANSWERED) (NEW))) TO "to@ya.ru")" diff --git a/imap_tools/__init__.py b/imap_tools/__init__.py index 308f874..1439fd7 100644 --- a/imap_tools/__init__.py +++ b/imap_tools/__init__.py @@ -1,7 +1,7 @@ -from .query import Q, AND, OR, NOT, H +from .query import AND, OR, NOT, Header, A, O, N, H from .mailbox import * from .message import * from .folder import * from .utils import * -__version__ = '0.16.1' +__version__ = '0.17.0' diff --git a/imap_tools/query.py b/imap_tools/query.py index 161aa61..a03e839 100644 --- a/imap_tools/query.py +++ b/imap_tools/query.py @@ -49,7 +49,17 @@ def combine_params(self) -> str: return 'NOT {}'.format(self.prefix_join('', itertools.chain(self.converted_strings, self.converted_params))) -Q = AND # Short alias +# Short alias set: +A = AND +O = OR # noqa +N = NOT + + +class Q(AND): + def __init__(self, *args, **kwargs): + import warnings + warnings.warn('alias Q are deprecated and will be removed soon, use A instead') + super().__init__(*args, **kwargs) class Header: @@ -304,4 +314,4 @@ def convert_uid(self, key, value): return 'UID {}'.format(self.cleaned_uid(key, value)) def convert_gmail_label(self, key, value): - return 'X-GM-LABELS {}'.format(quote(self.cleaned_str(key, value))) \ No newline at end of file + return 'X-GM-LABELS {}'.format(quote(self.cleaned_str(key, value))) diff --git a/release_notes.rst b/release_notes.rst index 6b7b521..c5446f2 100644 --- a/release_notes.rst +++ b/release_notes.rst @@ -1,3 +1,8 @@ +0.17.0 +====== +* Query builder: removed Q alias for AND +* Query builder: added new aliases: A for AND, O for OR, N for NOT + 0.16.1 ====== * Added X-GM-LABELS support to query builder (gmail_label) diff --git a/setup.py b/setup.py index 46b6a16..8d87c73 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def get_version(package: str) -> str: author='v.kaukin', author_email='KaukinVK@ya.com', description='Working with email and mailbox using IMAP protocol.', - keywords=['imap', 'imap-client', 'python3', 'email'], + keywords=['imap', 'imap-client', 'python3', 'python', 'email'], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", diff --git a/tests/test_query.py b/tests/test_query.py index 39cd72a..602996a 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,7 +1,7 @@ import unittest import datetime as dt -from imap_tools.query import ParamConverter, Q, AND, OR, NOT, H +from imap_tools.query import ParamConverter, A, AND, OR, NOT, H class QueryTest(unittest.TestCase): @@ -33,49 +33,49 @@ def not_fetch(): cleaned_fn('key_does_not_matter', bad) def test_converters(self): - self.assertEqual(Q(answered=True), '(ANSWERED)') - self.assertEqual(Q(answered=False), '(UNANSWERED)') - self.assertEqual(Q(seen=True), '(SEEN)') - self.assertEqual(Q(seen=False), '(UNSEEN)') - self.assertEqual(Q(flagged=True), '(FLAGGED)') - self.assertEqual(Q(flagged=False), '(UNFLAGGED)') - self.assertEqual(Q(draft=True), '(DRAFT)') - self.assertEqual(Q(draft=False), '(UNDRAFT)') - self.assertEqual(Q(deleted=True), '(DELETED)') - self.assertEqual(Q(deleted=False), '(UNDELETED)') - self.assertEqual(Q(keyword='KEY1'), '(KEYWORD KEY1)') - self.assertEqual(Q(no_keyword='KEY2'), '(UNKEYWORD KEY2)') - - self.assertEqual(Q(from_='from@ya.ru'), '(FROM "from@ya.ru")') - self.assertEqual(Q(to='to@ya.ru'), '(TO "to@ya.ru")') - self.assertEqual(Q(subject='hello'), '(SUBJECT "hello")') - self.assertEqual(Q(body='body text'), '(BODY "body text")') - self.assertEqual(Q(body='hi'), '(BODY "hi")') - self.assertEqual(Q(text='"quoted text"'), '(TEXT "\\"quoted text\\"")') - self.assertEqual(Q(text='hi'), '(TEXT "hi")') - self.assertEqual(Q(bcc='bcc@ya.ru'), '(BCC "bcc@ya.ru")') - self.assertEqual(Q(cc='cc@ya.ru'), '(CC "cc@ya.ru")') - - self.assertEqual(Q(date=dt.date(2000, 3, 15)), '(ON 15-Mar-2000)') - self.assertEqual(Q(date_gte=dt.date(2000, 3, 15)), '(SINCE 15-Mar-2000)') - self.assertEqual(Q(date_lt=dt.date(2000, 3, 15)), '(BEFORE 15-Mar-2000)') - self.assertEqual(Q(sent_date=dt.date(2000, 3, 15)), '(SENTON 15-Mar-2000)') - self.assertEqual(Q(sent_date_gte=dt.date(2000, 3, 15)), '(SENTSINCE 15-Mar-2000)') - self.assertEqual(Q(sent_date_lt=dt.date(2000, 3, 15)), '(SENTBEFORE 15-Mar-2000)') - - self.assertEqual(Q(size_gt=1024), '(LARGER 1024)') - self.assertEqual(Q(size_lt=512), '(SMALLER 512)') - - self.assertEqual(Q(new=True), '(NEW)') - self.assertEqual(Q(old=True), '(OLD)') - self.assertEqual(Q(recent=True), '(RECENT)') - self.assertEqual(Q(all=True), '(ALL)') - - self.assertEqual(Q(header=H('X-Google-Smtp-Source', '123')), '(HEADER "X-Google-Smtp-Source" "123")') - self.assertEqual(Q(uid='1,2'), '(UID 1,2)') - self.assertEqual(Q(uid=['3', '4']), '(UID 3,4)') - - self.assertEqual(Q(gmail_label="TestLabel"), '(X-GM-LABELS "TestLabel")') + self.assertEqual(A(answered=True), '(ANSWERED)') + self.assertEqual(A(answered=False), '(UNANSWERED)') + self.assertEqual(A(seen=True), '(SEEN)') + self.assertEqual(A(seen=False), '(UNSEEN)') + self.assertEqual(A(flagged=True), '(FLAGGED)') + self.assertEqual(A(flagged=False), '(UNFLAGGED)') + self.assertEqual(A(draft=True), '(DRAFT)') + self.assertEqual(A(draft=False), '(UNDRAFT)') + self.assertEqual(A(deleted=True), '(DELETED)') + self.assertEqual(A(deleted=False), '(UNDELETED)') + self.assertEqual(A(keyword='KEY1'), '(KEYWORD KEY1)') + self.assertEqual(A(no_keyword='KEY2'), '(UNKEYWORD KEY2)') + + self.assertEqual(A(from_='from@ya.ru'), '(FROM "from@ya.ru")') + self.assertEqual(A(to='to@ya.ru'), '(TO "to@ya.ru")') + self.assertEqual(A(subject='hello'), '(SUBJECT "hello")') + self.assertEqual(A(body='body text'), '(BODY "body text")') + self.assertEqual(A(body='hi'), '(BODY "hi")') + self.assertEqual(A(text='"quoted text"'), '(TEXT "\\"quoted text\\"")') + self.assertEqual(A(text='hi'), '(TEXT "hi")') + self.assertEqual(A(bcc='bcc@ya.ru'), '(BCC "bcc@ya.ru")') + self.assertEqual(A(cc='cc@ya.ru'), '(CC "cc@ya.ru")') + + self.assertEqual(A(date=dt.date(2000, 3, 15)), '(ON 15-Mar-2000)') + self.assertEqual(A(date_gte=dt.date(2000, 3, 15)), '(SINCE 15-Mar-2000)') + self.assertEqual(A(date_lt=dt.date(2000, 3, 15)), '(BEFORE 15-Mar-2000)') + self.assertEqual(A(sent_date=dt.date(2000, 3, 15)), '(SENTON 15-Mar-2000)') + self.assertEqual(A(sent_date_gte=dt.date(2000, 3, 15)), '(SENTSINCE 15-Mar-2000)') + self.assertEqual(A(sent_date_lt=dt.date(2000, 3, 15)), '(SENTBEFORE 15-Mar-2000)') + + self.assertEqual(A(size_gt=1024), '(LARGER 1024)') + self.assertEqual(A(size_lt=512), '(SMALLER 512)') + + self.assertEqual(A(new=True), '(NEW)') + self.assertEqual(A(old=True), '(OLD)') + self.assertEqual(A(recent=True), '(RECENT)') + self.assertEqual(A(all=True), '(ALL)') + + self.assertEqual(A(header=H('X-Google-Smtp-Source', '123')), '(HEADER "X-Google-Smtp-Source" "123")') + self.assertEqual(A(uid='1,2'), '(UID 1,2)') + self.assertEqual(A(uid=['3', '4']), '(UID 3,4)') + + self.assertEqual(A(gmail_label="TestLabel"), '(X-GM-LABELS "TestLabel")') def test_format_date(self): self.assertEqual(ParamConverter.format_date(dt.date(2000, 1, 15)), '15-Jan-2000') @@ -85,12 +85,12 @@ def test_logic_operators(self): self.assertEqual(AND(text='hello', new=True), '(TEXT "hello" NEW)') self.assertEqual(OR(text='hello', new=True), '(OR TEXT "hello" NEW)') self.assertEqual(NOT(text='hello', new=True), 'NOT (TEXT "hello" NEW)') - self.assertEqual(Q(AND(to='one@mail.ru'), AND(to='two@mail.ru')), '((TO "one@mail.ru") (TO "two@mail.ru"))') + self.assertEqual(A(AND(to='one@mail.ru'), AND(to='two@mail.ru')), '((TO "one@mail.ru") (TO "two@mail.ru"))') self.assertEqual( OR(date=[dt.date(2019, 10, 1), dt.date(2019, 10, 10), dt.date(2019, 10, 15), dt.date(2019, 10, 20)]), '(OR OR OR ON 1-Oct-2019 ON 10-Oct-2019 ON 15-Oct-2019 ON 20-Oct-2019)') self.assertEqual( - Q(OR(from_='from@ya.ru', text='"the text"'), NOT(OR(Q(answered=False), Q(new=True))), to='to@ya.ru'), + A(OR(from_='from@ya.ru', text='"the text"'), NOT(OR(A(answered=False), A(new=True))), to='to@ya.ru'), '((OR FROM "from@ya.ru" TEXT "\\"the text\\"") NOT ((OR (UNANSWERED) (NEW))) TO "to@ya.ru")') def test_header(self):