Skip to content

Commit 86254e5

Browse files
authored
Merge pull request #1 from RedTurtle/cekk_refactoring
Refactoring with adapters
2 parents e6b86ff + 178f5d5 commit 86254e5

File tree

15 files changed

+586
-257
lines changed

15 files changed

+586
-257
lines changed

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,6 @@
7373
target = plone
7474
[console_scripts]
7575
update_locale = redturtle.rsync.locales.update:update_locale
76+
redturtle_rsync = redturtle.rsync.scripts.rsync:main
7677
""",
7778
)

src/redturtle/rsync/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
from zope.i18nmessageid import MessageFactory
44

55

6-
_ = MessageFactory('redturtle.rsync')
6+
_ = MessageFactory("redturtle.rsync")

src/redturtle/rsync/adapters/__init__.py

Whitespace-only changes.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
from pathlib import Path
2+
from redturtle.rsync.interfaces import IRedturtleRsyncAdapter
3+
from requests.adapters import HTTPAdapter
4+
from requests.packages.urllib3.util.retry import Retry
5+
from zope.component import adapter
6+
from zope.interface import implementer
7+
from zope.interface import Interface
8+
9+
import json
10+
import requests
11+
12+
13+
class TimeoutHTTPAdapter(HTTPAdapter):
14+
def __init__(self, *args, **kwargs):
15+
if "timeout" in kwargs:
16+
self.timeout = kwargs["timeout"]
17+
del kwargs["timeout"]
18+
super(TimeoutHTTPAdapter, self).__init__(*args, **kwargs)
19+
20+
def send(self, request, **kwargs):
21+
timeout = kwargs.get("timeout")
22+
if timeout is None:
23+
kwargs["timeout"] = self.timeout
24+
return super(TimeoutHTTPAdapter, self).send(request, **kwargs)
25+
26+
27+
@implementer(IRedturtleRsyncAdapter)
28+
@adapter(Interface, Interface)
29+
class RsyncAdapterBase:
30+
"""
31+
This is the base class for all rsync adapters.
32+
It provides a common interface for all adapters and some default
33+
implementations of the methods.
34+
Default methods works with some data in restapi-like format.
35+
"""
36+
37+
def __init__(self, context, request):
38+
self.context = context
39+
self.request = request
40+
41+
def requests_retry_session(
42+
self,
43+
retries=3,
44+
backoff_factor=0.3,
45+
status_forcelist=(500, 501, 502, 503, 504),
46+
timeout=5.0,
47+
session=None,
48+
):
49+
"""
50+
https://dev.to/ssbozy/python-requests-with-retries-4p03
51+
"""
52+
session = session or requests.Session()
53+
retry = Retry(
54+
total=retries,
55+
read=retries,
56+
connect=retries,
57+
backoff_factor=backoff_factor,
58+
status_forcelist=status_forcelist,
59+
)
60+
# adapter = HTTPAdapter(max_retries=retry)
61+
http_adapter = TimeoutHTTPAdapter(max_retries=retry, timeout=timeout)
62+
session.mount("http://", http_adapter)
63+
session.mount("https://", http_adapter)
64+
return session
65+
66+
def log_item_title(self, start, options):
67+
"""
68+
Return the title of the log item for the rsync command.
69+
"""
70+
return f"Report sync {start.strftime('%d-%m-%Y %H:%M:%S')}"
71+
72+
def set_args(self, parser):
73+
"""
74+
Set some additional arguments for the rsync command.
75+
76+
For example:
77+
parser.add_argument(
78+
"--import-type",
79+
choices=["xxx", "yyy", "zzz"],
80+
help="Import type",
81+
)
82+
"""
83+
return
84+
85+
def get_data(self, options):
86+
"""
87+
Convert the data to be used for the rsync command.
88+
Return:
89+
- data: the data to be used for the rsync command
90+
- error: an error message if there was an error, None otherwise
91+
"""
92+
error = None
93+
data = None
94+
# first, read source data
95+
if getattr(options, "source_path", None):
96+
file_path = Path(options.source_path)
97+
if file_path.exists() and file_path.is_file():
98+
with open(file_path, "r") as f:
99+
try:
100+
data = json.load(f)
101+
except json.JSONDecodeError:
102+
data = f.read()
103+
else:
104+
error = f"Source file not found in: {file_path}"
105+
return data, error
106+
elif getattr(options, "source_url", None):
107+
http = self.requests_retry_session(retries=7, timeout=30.0)
108+
response = http.get(options.source_url)
109+
if response.status_code != 200:
110+
error = f"Error getting data from {options.source_url}: {response.status_code}"
111+
return data, error
112+
if "application/json" in response.headers.get("Content-Type", ""):
113+
try:
114+
data = response.json()
115+
except ValueError:
116+
data = response.content
117+
else:
118+
data = response.content
119+
120+
if data:
121+
data, error = self.convert_source_data(data)
122+
return data, error
123+
124+
def convert_source_data(self, data):
125+
"""
126+
If needed, convert the source data to a format that can be used by the rsync command.
127+
"""
128+
return data, None
129+
130+
def find_item_from_row(self, row):
131+
"""
132+
Find the item in the context from the given row of data.
133+
This method should be implemented by subclasses to find the specific type of content item.
134+
"""
135+
raise NotImplementedError()
136+
137+
def create_item(self, row, options):
138+
"""
139+
Create a new content item from the given row of data.
140+
This method should be implemented by subclasses to create the specific type of content item.
141+
"""
142+
raise NotImplementedError()
143+
144+
def update_item(self, item, row):
145+
"""
146+
Update an existing content item from the given row of data.
147+
This method should be implemented by subclasses to update the specific type of content item.
148+
"""
149+
raise NotImplementedError()
150+
151+
def delete_items(self, data, sync_uids):
152+
"""
153+
params:
154+
- data: the data to be used for the rsync command
155+
- sync_uids: the uids of the items thata has been updated
156+
157+
Delete items if needed.
158+
This method should be implemented by subclasses to delete the specific type of content item.
159+
"""
160+
return
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<configure
2+
xmlns="http://namespaces.zope.org/zope"
3+
xmlns:zcml="http://namespaces.zope.org/zcml"
4+
i18n_domain="redturtle.rsync"
5+
>
6+
7+
<adapter factory=".adapter.RsyncAdapterBase" />
8+
</configure>

src/redturtle/rsync/browser/configure.zcml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@
22
xmlns="http://namespaces.zope.org/zope"
33
xmlns:browser="http://namespaces.zope.org/browser"
44
xmlns:plone="http://namespaces.plone.org/plone"
5-
i18n_domain="redturtle.rsync">
5+
i18n_domain="redturtle.rsync"
6+
>
67

78
<!-- Set overrides folder for Just-a-Bunch-Of-Templates product -->
8-
<include package="z3c.jbot" file="meta.zcml" />
9+
<include
10+
package="z3c.jbot"
11+
file="meta.zcml"
12+
/>
913
<browser:jbot
1014
directory="overrides"
1115
layer="redturtle.rsync.interfaces.IRedturtleRsyncLayer"
1216
/>
1317

1418
<!-- Publish static files -->
1519
<plone:static
20+
directory="static"
1621
name="redturtle.rsync"
1722
type="plone"
18-
directory="static"
1923
/>
2024

2125
</configure>

src/redturtle/rsync/configure.zcml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
44
xmlns:i18n="http://namespaces.zope.org/i18n"
55
xmlns:plone="http://namespaces.plone.org/plone"
6-
i18n_domain="redturtle.rsync">
6+
i18n_domain="redturtle.rsync"
7+
>
78

89
<i18n:registerTranslations directory="locales" />
910

@@ -16,23 +17,24 @@
1617
<!-- has to be loaded before permissions are used, so keep above views aso. -->
1718
<include file="permissions.zcml" />
1819

20+
<include package=".adapters" />
1921
<include package=".browser" />
2022

2123
<genericsetup:registerProfile
2224
name="default"
2325
title="redturtle.rsync"
24-
directory="profiles/default"
2526
description="Installs the redturtle.rsync add-on."
2627
provides="Products.GenericSetup.interfaces.EXTENSION"
28+
directory="profiles/default"
2729
post_handler=".setuphandlers.post_install"
2830
/>
2931

3032
<genericsetup:registerProfile
3133
name="uninstall"
3234
title="redturtle.rsync (uninstall)"
33-
directory="profiles/uninstall"
3435
description="Uninstalls the redturtle.rsync add-on."
3536
provides="Products.GenericSetup.interfaces.EXTENSION"
37+
directory="profiles/uninstall"
3638
post_handler=".setuphandlers.uninstall"
3739
/>
3840

src/redturtle/rsync/interfaces.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,71 @@
11
# -*- coding: utf-8 -*-
22
"""Module where all interfaces, events and exceptions live."""
3-
3+
from zope.interface import Interface
44
from zope.publisher.interfaces.browser import IDefaultBrowserLayer
55

66

77
class IRedturtleRsyncLayer(IDefaultBrowserLayer):
88
"""Marker interface that defines a browser layer."""
9+
10+
11+
class IRedturtleRsyncAdapter(Interface):
12+
"""Marker interface for the redturtle rsync adapter."""
13+
14+
def __init__(context, request):
15+
"""Initialize the adapter with the given context and request."""
16+
17+
def log_item_title(start, options):
18+
"""
19+
Return the title of the log item for the rsync command.
20+
"""
21+
22+
def set_args(parser):
23+
"""
24+
Set some additional arguments for the rsync command.
25+
"""
26+
27+
def get_data(options):
28+
"""
29+
Set some additional arguments for the rsync command.
30+
"""
31+
32+
def handle_row(row):
33+
"""
34+
Method to handle a row of data.
35+
For example it could do the following steps:
36+
- check if there is already a content item with the same id
37+
- if not, create a new content item
38+
- if yes, update the existing content item
39+
40+
It should return the content item created or updated and the status of the operation.
41+
The status could be one of the following:
42+
- "created": a new content item was created
43+
- "updated": an existing content item was updated
44+
- "skipped": the content item was skipped because it already exists and is up to date
45+
- "error": an error occurred while processing the content item
46+
47+
for example:
48+
return {'item': content_item, 'status': status}
49+
"""
50+
51+
def create_item(row):
52+
"""
53+
Create a new content item from the given row of data.
54+
This method should be implemented by subclasses to create the specific type of content item.
55+
"""
56+
57+
def update_item(item, row):
58+
"""
59+
Update an existing content item from the given row of data.
60+
This method should be implemented by subclasses to update the specific type of content item.
61+
"""
62+
63+
def delete_items(data, sync_uids):
64+
"""
65+
params:
66+
- data: the data to be used for the rsync command
67+
- sync_uids: the uids of the items thata has been updated
68+
69+
Delete items if needed.
70+
This method should be implemented by subclasses to delete the specific type of content item.
71+
"""

src/redturtle/rsync/locales/update.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@
55
import subprocess
66

77

8-
domain = 'redturtle.rsync'
9-
os.chdir(pkg_resources.resource_filename(domain, ''))
10-
os.chdir('../../../')
11-
target_path = 'src/redturtle/rsync/'
12-
locale_path = target_path + 'locales/'
13-
i18ndude = './bin/i18ndude'
8+
domain = "redturtle.rsync"
9+
os.chdir(pkg_resources.resource_filename(domain, ""))
10+
os.chdir("../../../")
11+
target_path = "src/redturtle/rsync/"
12+
locale_path = target_path + "locales/"
13+
i18ndude = "./bin/i18ndude"
1414

1515
# ignore node_modules files resulting in errors
1616
excludes = '"*.html *json-schema*.xml"'
1717

1818

1919
def locale_folder_setup():
2020
os.chdir(locale_path)
21-
languages = [d for d in os.listdir('.') if os.path.isdir(d)]
21+
languages = [d for d in os.listdir(".") if os.path.isdir(d)]
2222
for lang in languages:
2323
folder = os.listdir(lang)
24-
if 'LC_MESSAGES' in folder:
24+
if "LC_MESSAGES" in folder:
2525
continue
2626
else:
27-
lc_messages_path = lang + '/LC_MESSAGES/'
27+
lc_messages_path = lang + "/LC_MESSAGES/"
2828
os.mkdir(lc_messages_path)
29-
cmd = 'msginit --locale={0} --input={1}.pot --output={2}/LC_MESSAGES/{3}.po'.format( # NOQA: E501
29+
cmd = "msginit --locale={0} --input={1}.pot --output={2}/LC_MESSAGES/{3}.po".format( # NOQA: E501
3030
lang,
3131
domain,
3232
lang,
@@ -37,11 +37,11 @@ def locale_folder_setup():
3737
shell=True,
3838
)
3939

40-
os.chdir('../../../../')
40+
os.chdir("../../../../")
4141

4242

4343
def _rebuild():
44-
cmd = '{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot --exclude {excludes} --create {domain} {target_path}'.format( # NOQA: E501
44+
cmd = "{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot --exclude {excludes} --create {domain} {target_path}".format( # NOQA: E501
4545
i18ndude=i18ndude,
4646
locale_path=locale_path,
4747
domain=domain,
@@ -55,7 +55,7 @@ def _rebuild():
5555

5656

5757
def _sync():
58-
cmd = '{0} sync --pot {1}/{2}.pot {3}*/LC_MESSAGES/{4}.po'.format(
58+
cmd = "{0} sync --pot {1}/{2}.pot {3}*/LC_MESSAGES/{4}.po".format(
5959
i18ndude,
6060
locale_path,
6161
domain,

src/redturtle/rsync/permissions.zcml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<configure
2-
xmlns="http://namespaces.zope.org/zope"
3-
xmlns:zcml="http://namespaces.zope.org/zcml"
4-
i18n_domain="plone">
2+
xmlns="http://namespaces.zope.org/zope"
3+
xmlns:zcml="http://namespaces.zope.org/zcml"
4+
i18n_domain="plone"
5+
>
56

67
<configure zcml:condition="installed AccessControl.security">
7-
<!-- -*- extra stuff goes here -*- -->
8+
<!-- -*- extra stuff goes here -*- -->
89

910

1011
</configure>

0 commit comments

Comments
 (0)